기본 콘텐츠로 건너뛰기

[vuejs 관련] Selectbox component

Selectbox component


component .vue파일

<template>
  <div class="m_selectbox_cm">
    <button class="multi_del" v-if="info.multiple && info.multitxt.length > 1" 
              @mouseenter="info.multidel = true" @mouseleave="info.multidel = false" 
              @click="multiDel">
       <img src="@/assets/images/icon_close.svg" alt="">
    </button>
    <a role="button" tabindex="1" :id="id" class="select" 
              @click="showList" @blur="optionHide" 
              @mouseenter="info.blurbo = false" @mouseleave="info.blurbo = true">
       {{ info.multiple ? info.selecmulti : info.selecone }}
    </a>
    <div class="optionwrap" v-if="info.showopt">
      <div class="optionslist">
        <button class="opt" :class="{ on: seleclass(list.value) }" v-for="(list, idx) in props.options" :key="idx" 
                  @click="changeVal(list)" @blur.self="optionHideSend" 
                  @mouseenter="info.blurbo = false" @mouseleave="info.blurbo = true">
           {{ list.txt }}
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, watch, computed } from 'vue'

const props = defineProps({
  options: Object,
  id: String,
  val: undefined,
  multi: false,
  closeoption: false
})

const emit = defineEmits(['change'])

const info = ref({
  showopt: false,// 옵션 목록 보이기 여부
  multiple: false,// 여러개 선택 여부
  multitxt: [],// 여러개 선택 시 문구

  // 선택한 옵션
  selecone: computed(() => {
    return props?.options?.filter(d => d.value === props?.val)[0]?.txt
  }),
  selecmulti: computed(() => {
    return info.value.multitxt.length > 1 ? 
              info.value.multitxt[0] + ", + " + (info.value.multitxt.length - 1) : 
              info.value.multitxt[0]
  }),

  // 여러개 옵션일 때 옵션 영역 바깥 클릭 시 옵션 닫기
  blurbo: true,
  // 옵션 닫기
  selectoption: false,
  // 선택한 옵션 초기화
  multidel: false,
})
info.value.selectoption = props.closeoption

// 선택된 옵션(단일, 여러개)
const selectoption = ref([])
selectoption.value = props.val

// 여러개 선택
info.value.multiple = props.multi
// 여러개 선택일 때 내용 입력
if(info.value.multiple) {
  selectoption?.value.filter((val) => {
    props.options.filter((val2) => {
      if(val === val2.value) {
        info.value.multitxt.push(val2.txt);
      }
    })
  })
}

// 선택된 옵션. 클래스 적용
const seleclass = computed(() => {
  return (val) => {
    return info.value.multiple ?
      selectoption.value.filter((va) => va === val).length : // 옵션이 여러개 일 때
      selectoption.value === val ? true : false // 단일 옵션일 때
  }
})

watch(props, (e) => {
  if(e.val) {
    selectoption.value = e.val
  }
  if(e.multi) {
    info.value.multiple = true
  }
  if(e.closeoption) {
    info.value.selectoption = true;
    info.value.showopt = false
  }
})
watch(() => info.value.selectoption, (nv, ov) => {
  if(info.value.selectoption) {
    info.value.selectoption = false;
    info.value.showopt = false
  }
})

// 옵션 목록 보기(true / false)
const showList = () => {
  info.value.showopt = !info.value.showopt
}
// 옵션 선택
const changeVal = (n) => {
  info.value.selectclose = true;
  // 여러개 선택 시
  if(info.value.multiple) {
    // 옵션의 값이 0, '0', '', null 이 아닐 때
    if(n.value !== 0 && n.value !== '0' && n.value !== '' && n.value !== null) {
      // value, text
      if(selectoption.value.indexOf(n.value) < 0) {//(info.value.multitxt.indexOf(n.txt) < 0)
        selectoption.value.push(n.value)
        info.value.multitxt.push(n.txt)
      } else {
        selectoption.value.splice(selectoption.value.indexOf(n.value), 1)
        info.value.multitxt.splice(info.value.multitxt.indexOf(n.txt), 1)
      }
    }

    // 옵션 선택 시 기본 옵션 제거
    info.value.multitxt.some((ele, idx) => {
      if(props.options[0].txt === ele) {
        info.value.multitxt.splice(idx, 1);
        selectoption.value.splice(idx, 1);
      }
    })

    // // 선택된 옵션이 다 없어지지 않게 마지막 옵션이 선택되게
    // if(selectoption.value.length === 0) {
    //   selectoption.value.push(n.value)
    //   info.value.multitxt.push(n.txt)
    // }

    // 선택된 옵션이 다 없어지면 기본 옵션이 선택되게
    if(selectoption.value.length === 0) {
      selectoption.value.push(props.options[0].value);
      info.value.multitxt.push(props.options[0].txt);
    }
  }
  // 단일 선택 시
  else {
    info.value.showopt = false
    emit('change', n)
  }
}

// 옵션(버튼) 밖 클릭 시
const optionHideSend = () => {
  if(info.value.multiple && info.value.blurbo && !info.value.multidel) {
    info.value.showopt = false;
    emit('change', selectoption.value)
  }
}

// 선택 버튼 밖 클릭 시
const optionHide = () => {
  if(info.value.blurbo) {
    info.value.showopt = false;
  }
}

// 여러개 선택 시 두개 이상 선택했으면 선택목록 초기화 버튼 보이기
const multiDel = () => {
  selectoption.value = [props.options[0].value];
  info.value.multitxt = [props.options[0].txt];
  info.value.multidel = false;
  optionHideSend();
}
</script>

<style scoped src="./SelectboxCm.scss"></style>
<!--
  // iOS에서는 <button>에 focus가 안되어서 <a role="button" tabindex="1"> 이런식으로 변경
  // 부모 컴포넌트에서 사용 시
  # options : selectbox의 option 값. options.value, options.txt
  # val : 보여지는 option 값
  # multi : 여러 옵션 선택( :multi="true" )
  ##== multi ref: ["opt1", "opt2", ...]

  <SelectboxCm :options="[{value: 'value값', txt:'txt문구'}]" :val="미리 보여지는 option값" />

  ex : <SelectboxCm :options="[{ value: 'm1', txt: 'ㅇㅇㅇ' }, { value: 'm2', txt: 'ㅍㅍㅍ' }, { value: 'm3', txt: 'ㅎㅎㅎ' }]" @change="selecManager" />
 -->


불러오는 .vue 파일에

<template>
  ...
  <Selectbox :options="info.opt" :val="info.selectedoption" @change="selec" />
  ...
  <SelectboxCm :multi="true" :options="info.optmulti" :val="info.selecmulti" @change="selecMulti" />
  ...
</template>

<script setup>
import { ref } from "vue";
import Selectbox from "@/components/Selectbox.vue";

//
const info = ref({
  opt: [
    { value: "", txt: "전체" },
    { value: "opt1", txt: "내용1" },
    { value: "opt2", txt: "내용2" },
    { value: "opt3", txt: "내용3" }
  ],
  selectedoption: "opt1",
  ...
  optmulti: [
    { value: "", txt: "전체" },
    { value: "optm1", txt: "내용11" },
    { value: "optm2", txt: "내용22" },
    { value: "optm3", txt: "내용33" }
  ],
  selecmulti: ["optm1"]
})

//
const selec = (e) => {
  info.value.selectedoption = e.value
  console.log(info.value.selectedoption, e.txt);
}
...
// 
const selecMulti = (e) => {
  info.value.selecmulti = e;
  console.log(info.value.selecmulti);
}


참고용 css

.m_selectbox_cm {
  position: relative;
  min-width: 12rem;
  height: 3.2rem;
  border: 1px solid $c_dc;
  border-radius: 0.4rem;
  &::after {
    content: "";
    margin: auto;
    position: absolute;
    top: 0.3rem;
    right: 1.2rem;
    bottom: 0;
    width: 0.9rem;
    height: 0.5rem;
    background: url(@/assets/images/icon_select.svg) no-repeat center;
    pointer-events: none;
  }
  .select {
    padding: 0 3.2rem 0 1.6rem;
    width: 100%;
    height: 100%;
    line-height: 3rem;
    font-size: 1.2rem;
    color: $c_42;
    text-align: left;
    font-weight: 500;
    letter-spacing: -0.02em;
    cursor: pointer;
  }
  .optionwrap {
    position: absolute;
    top: 4.2rem;
    left: -1px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    width: calc(100% + 2px); // m_selectbox_cm의 border값 포함하려고
    box-shadow: 0 0 1.2rem rgba(20, 82, 143, 0.2);
    border-radius: 0.8rem;
    z-index: 9;
    .optionslist {
      width: 100%;
      max-height: 20rem;
      overflow-y: auto;
      background-color: #fff;
      border-radius: 0.8rem;
    }
    .opt {
      padding-left: 2rem;
      position: relative;
      width: 100%;
      height: 3.2rem;
      font-size: 1.4rem;
      font-weight: 500;
      color: $c_82;
      line-height: 3.2rem;
      text-align: left;
      ~ .opt {
        &::before {
          content: "";
          position: absolute;
          left: 0;
          top: 0;
          width: 100%;
          height: 1px;
          background-color: $c_f1;
        }
      }
      &.on {
        color: $c_pri;
        background-color: #f6faff;
      }
      &:hover {
        background-color: #f6faff;
      }
    }
  }
  &.middle {
    height: 4.4rem;
    border-radius: 0.8rem;
    .select {
      padding: 0 3rem 0 2rem;
      line-height: 4.2rem;
      font-size: 1.4rem;
    }
    .optionwrap {
      top: 5.4rem;
      .opt {
        height: 4.4rem;
        line-height: 4.4rem;
      }
    }
  }
  &.middle48 {
    min-width: 17rem;
    height: 4.8rem;
    border-radius: 0.8rem;
    .select {
      padding: 0 3rem 0 2rem;
      line-height: 4.6rem;
      font-size: 1.4rem;
    }
    .optionwrap {
      top: 5.8rem;
      .opt {
        height: 4.8rem;
        line-height: 4.8rem;
      }
    }
  }
  ~ .m_selectbox {
    margin-left: 1rem;
  }
  &.big {
    height: 5.2rem;
    min-width: 20rem;
    border-radius: 0.8rem;
    .select {
      padding: 0 3.5rem 0 1.9rem;
    }
    &::after {
      right: 2rem;
    }
    &.more {
      height: 5.6rem;
      .optionwrap {
        top: 6.6rem;
        .opt {
          height: 5.6rem;
          line-height: 5.6rem;
        }
      }
    }
    .optionwrap {
      top: 6.2rem;
      .opt {
        height: 5.2rem;
        line-height: 5.2rem;
      }
    }
  }
  &.noact {
    background-color: $c_f1;
    .select {
      color: $c_82;
    }
    &::after {
      color: $c_d9;
    }
  }
  &.noborder {
    min-width: 8.4rem;
    border: none;
    &::after {
      right: 0.4rem;
    }
    .select {
      padding-left: 0;
      padding-right: 1.6rem;
      line-height: 3.2rem;
    }
    .optionwrap {
      left: -0.8rem;
      top: initial;
      bottom: 4.4rem;
      .optionslist {
        min-width: 9.4rem;
        max-height: 16rem;
      }
      .opt {
        padding-left: 1rem;
        font-size: 1.2rem;
        letter-spacing: -0.24px;
      }
    }
  }
  .multi_del {
    margin: auto;
    padding: 0 1rem;
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0.2rem;
    z-index: 1;
  }
}


Selectbox component


끝.