[Vue.js] v-model 을 이용한 체크박스 컴포넌트 구현 -양방향 바인딩

hyejinJo·2023년 3월 24일
1

Vue

목록 보기
6/7
post-thumbnail

v-model 을 이용해서 체크박스를 만들어보는 과제를 받았다. v-model 에 대한 개념을 잘 파악한 뒤, 우여곡절 끝에 체크박스를 구현하는데 성공했다. 그리고 여기서 그치지 않고 전체로 체크되는 체크박스도 한 번 구현해보았다.

  • vue 에서는 부모에서 자식 컴포넌트로 데이터를 보낼 땐 props를, 자식에서 부모 컴포넌트로 데이터를 보낼땐 $emit 을 사용한다.
  • 웬만해선 데이터를 바꿔주는 method 는 부모가 아닌 자식 컴포넌트에서 실행되도록 하고 자식 컴포넌트는 부모에 데이터만 건네준다. (부모쪽에서 실행하면 자식 컴포넌트 하나를 부모에 달때마다 method 를 부모에 하나씩 계속 추가해줘야하기 때문에 비효율적임) 즉 자식 컴포넌트에서 부모 컴포넌트에 이벤트까지 넘겨주는 것은 지양한다.

부모 컴포넌트

  • v-model 은 부모 컴포넌트의 value 로 들어감
  • 체크박스는 v-for 문을 돌려 여러 개로 생성
  • 전체 체크박스 기능도 추가
// index.vue

<template>
  <div id="checkbox">
    <h1>Checkbox</h1>
    // 전체 체크박스
    <checkboxAll v-model="checkedNames" label="전체 선택" :name-list="nameList" />
    // 일반 체크박스(list) 
    <ul id="checkbox">
      <li v-for="(name, index) in nameList" :key="name">
        <Checkbox
          v-model="checkedNames"
          label="체크박스"
          :name="name"
          :index="index" />
        // checkName() 이 실행되고 v-model 을 통해 checkName() 의 결과값인 arr 배열데이터가 checkedNames 에 들어감
      </li>
    </ul>
    <span>{{ checkedNames }}</span>
  </div>
</template>

<script>
import checkbox from '../components/checkbox.vue'

export default {
  data() {
    return {
      nameList: ['Jack','John','Mike'],
      // label 은 보통 한글로 쓰이기 때문에(그거야 한국이니까...)
      // 저렇게 문자열로만 적는 것이 아닌, 한글로 표기될 label 용 따로,
      // 서버로 실제로 옮겨지는 데이터 value 값용 따로 설정해서 배열의
      // 요소를 하나의 객체데이터로 만들어놓는 것이 좋다.
      // nameList: [
      //   {label: '잭', value: 'Jack'},
      //   {label: '존', value: 'John'},
      //   {label: '마이크', value: 'Mike'},
      // ],
      checkedNames: []
    }
  },
  component: {
    checkbox
  }
}
</script>

자식 컴포넌트 - 일반 체크박스

  • @input 이 아닌 @change 로 하여 change 이벤트를 통해 요소 변경이 끝나면 이벤트가 발생하도록 함
  • 체크된 값이면 isChecked 가 true 값이 되며, 이러한 체크 여부 변수를 통해 클래스 바인딩 실행 혹은 checked 유무를 표현할 수 있다.
// checkbox.vue

<template>
  <label :for="name">
    <input
      :id="name"
      :class="{'active': isChecked}"
      :checked="isChecked"
      type="checkbox"
      :value="name" 
	  @change="checkName" />
	// true 나 false 를 반환해서 클래스 바인딩을 통해 체크된(active) input 디자인 표현 -->
	// <em></em>{{ checkedNames.some(e => e === name) }}-->
	// checkedNames.some(e => e === name) : boolean 반환, class 유무가 정해짐-->
    <em></em>
    <span>{{ label }}</span>
    <span>{{ index + 1 }}</span>
    <span>{{ name }}</span>
  </label>
</template>

<script>
export default {
  name: 'Checkbox',
  // v-model 은 value 값을 받는다
  model: {
    prop: 'checkedNames',
    event: 'changeForm'
  },
  props: {
		// prop 으로 받은 것은 props 옵션에도 따로 기재해야함
    checkedNames: {
      type: Array
    },
    label: {
      type: String,
      default: '',
    },
    name: {
      type: String,
      required: true,
    },
    index: {
      type: Number,
      required: true,
    }
  },
  computed: {
    isChecked() {
      return this.checkedNames.includes(this.name)
    }
  },
  methods: {
    checkName(e) {
      let arr = [...this.checkedNames]
      // 자식 컴포넌트에서 데이터를 함부로 직접 바꾸면 안되기 때문에 전개연산자로
      // 얕은 복사를 한 뒤 변수에 담고 해당 데이터를 가공한 뒤 다시 $emit 으로 전달
      if (arr.includes(e.target.value)) {
        arr = arr.filter(name => name !== e.target.value)
      } else {
        arr.push(e.target.value)
      }
      // 가공된 데이터 다시 전달
      this.$emit('changeForm', arr)
    }
  }
}
</script>

<style>
...
</style>

자식 컴포넌트에서 value 는 그냥 value 이며, 반복문으로 생성된 여러 input 의 value 들 중 event.target 을 통해 추출된 value 값은 [checkedNames 를 얕게 복사한 arr 라는 배열]에 추가 혹은 삭제된 후 새로 수정된 해당 배열(arr)이 다시 부모 컴포넌트의 v-model(value)로 전달됨

변경된 데이터가 부모에 반영되고, 해당 데이터가 또 다시 자식으로 넘어가 수정 반복 ⇒ 양방향 데이터 바인딩

자식 컴포넌트 - 전체 체크박스

// checkboxAll.vue

<template>
  <label for="chkAll">
    <input
      id="chkAll"
      :class="{'active': isChecked}"
      :checked="isChecked"
      type="checkbox"
      value="chkAll"
      @change="checkFormAll"
    >
    <em></em>
    <span>{{ label }}</span>
  </label>
</template>

<script>
export default {
  name: 'CheckboxAll',
  model: {
    prop: 'checkedNames',
    event: 'changeFormAll',
  },
  props: {
    checkedNames: {
      type: Array,
    },
    label: {
      type: String,
      default: '',
    },
    nameList: {
      type: Array,
    }
  },
  data() {
    return {

    }
  },
  computed: {
    isChecked() {
      return this.checkedNames.length === this.nameList.length
    }
  },
  methods: {
    // 이미 모든 요소가 체크되어 있을 때: 체크된 모든 요소 해제
    // 모든 요소가 체크되어있지 않을 때: 모든 요소 체크
    checkFormAll() {
      let arr1 = [...this.checkedNames]
      const arr2 = [...this.nameList]
      let union = []

      if (arr1.length === arr2.length) {
        arr1 = []
        union = []
      } else if (arr2.length > arr1.length) {
        const sum = arr1.concat(arr2)
        union = sum.filter((item, index) => sum.indexOf(item) === index)
      }

      this.$emit('changeFormAll', union)
    }
  },
}
</script>

결과

profile
FE Developer 💡

0개의 댓글