hoony's web study

728x90
반응형

 

그 동안 vuetify를 쓰며 봤던 slot!

예제만 대충 복사해서 쓰고 어떻게 동작하는지는 모르고 있었다. 

그동안 귀찮아서 안 찾아보고 있었는데 확장성 있는 컴포넌트를 만들게되며 이제야 찾아보게 되었다.

slot외에도 props, emit 컴포넌트를 만들때 사용되는 문법들을 한번 정리 해보고자 한다.

 

vuetify의 data-table 예제

 

 

앞으로 사용할 예제는 Options API가 아닌 Composition API를 사용할 것 이다. 

vue3에서는 기본적으로 제공해주지만, vue2를 사용 중이라면 라이브러리를 추가적으로 설치해주어야한다. 

npm install @vue/composition-api

설치 후 main.js에 세팅까지~

// main.js
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)

 

 

✨ props로 부모 > 자식요소에게 데이터/이벤트 전달

 

props은 부모요소가 자식요소에게 함수 또는 데이터를 전달해 줄 수 있다.

// 자식.vue
<template>
  <div> 
	<!--	가져온 데이터		-->
    <p>{{ data }}</p> 
	<!--	가져온 함수		-->
    <button @click="clickFunction">클릭!</button>
  </div>
</template>
<script setup>
// props 정의
const props = defineProps(['data', 'clickFunction']);
</script>
// 부모.vue
<template>
  <!--	자식요소에 정의한 이름 그대로 사용해야한다.	-->
  <Child :data="innerText" :click-function="innerFunction" />
</template>
<script setup>
import Child from '~/components/common/Child.vue';

const innerText = '내부에 전달할 데이터';
const innerFunction = () => {
  console.log('내부에 전달할 함수');
};
</script>

 

이때 props는 위와 같이 const props = defineProps(['data', 'clickFunction']) 배열 형태로 나타낼 수도 있지만, 다른 옵션들을 넣어줄 수 있다. 

 

1. 타입 고정

const props = defineProps({
  data: String,
  clickFunction: Function,
});

- String

- Number

- Array

- Object

- Data

- Function

- Symbol

vue는 위의 타입설정을 제공한다. 2개 이상의 타입일 경우 배열형태로도 넣을 수 있다. ex) [String, Number]

 

2. 그외 설정들(defqult, required,validator)

const props = defineProps({
  data: {
    type: String, // type설정
    required: true, // 필수값 체크
    validator: function (value) { // return 이 true여야함
      return ['success', 'warning', 'danger'].indexOf(value) !== -1;
    },
  },
  clickFunction: {
    type: Function, 
    default: () => {}, // props로 주지 않을경우 기본값 설정
    required: false,
  },
});

 

prop으로 데이터를 내려주지만, function의 매개변수를 통해서 자식에서 부모에게 데이터를 넘겨줄 수 있다.

// 자식.vue
<template>
  <div>   
    <button @click="clickFunction(childData)">클릭!</button>
  </div>
</template>
<script setup>
const childData = 'childData';
const props = defineProps({ 
  clickFunction: {
    type: Function,
    default: () => {}, 
  },
});
</script>

 

// 부모.vue
<template>
  <Child :data="innerText" :click-function="innerFunction" />
</template>
<script setup>
import Child from '~/components/common/Child.vue';
const innerFunction = (data) => {
  console.log('자식으로부터온 data: ' + data); // 자식으로부터온 data: childData
};

 

 

✨ emit으로 자식 > 부모 요소에게 이벤트 전달

 

emit은 prop과 반대로 자식에서 부모로 이벤트를 올려준다. 

// 자식.vue
<template>
  <div>
    <button @click="btnClick">click 이벤트 전달</button>
  </div>
</template>
<script setup>
const emit = defineEmits(['click:btnClick']); // emit 등록! 

const btnClick = (e) => {
  emit('click:btnClick', e); // emit으로 e를 넘겨준다. 
};
</script>

 

// 부모.vue
<template>
  <!-- 자식.vue에서 emit에 등록된 이름을 사용해주어야한다. -->
  <Child @click:btn-click="btnEvent" /> 
</template>
<script setup>
import Child from '~/components/common/Child.vue';

const btnEvent = (e) => {
  console.log(e); // 자식요소의 click event 객체를 그대로 받을 수 있다. 
};
</script>

 

 

 

✨ v-model 로 데이터 바인딩 (modelValue)

 

위의 prop과 emit을 이용하면 v-model 과 같이 양방향 바인딩을 구현할 수 있다.

prop을 통해서 초기값을 부모로부터 자식으로 보낸 후, @input 이벤트를 통해서 입력값에 대한 event.target.value를 자식에서 부모로 올려주는 방식이다.

// 자식.vue
<template> 
  <input :value="modelValue" @input="changeData" />
</template>
<script setup>
const emit = defineEmits(['update:modelValue']); 
// v-model이라는 이름으로 사용하기 위해서는 "update:modelValue"을 사용해주어야한다.
const props = defineProps(['modelValue']);

const changeData = (e) => {ㄴ
  emit('update:modelValue', e.target.value);
  // 이처럼 emit으로 event 외에 원하는 값을 넘겨줄 수 있다.
};
</script>

 

// 부모.vue
<template>
  <Child v-model="data" />
</template>
<script setup>
import Child from '~/components/common/Child.vue';

const data = ref('');

// 데이터 변경을 확인하기 위한 코드
watch(data, () => {
  console.log(data.value);
});
</script>

 

이렇게 부모와 자식요소간에 데이터와 이벤트를 넘겨주고 받을 수 있다.

 

 

✨ slots으로 확장성있는 컴포넌트 만들기

 

slots는 부모에서 자식으로 html element를 넘겨줄 수 있다. 

// 자식.vue
<template>
  <div>
    <h1>Header</h1>
    <main>
      <slot /> <!-- 넘겨받을 위치 -->
    </main>
  </div>
</template>

 

// 부모.vue
<template>
  <Child>
    <div>... 넘겨줄 요소들 <span>abc</span></div>
  </Child>
</template>
<script setup>
import Child from '~/components/common/Child.vue';
</script>

 

이렇게 <Child> 태그 아래 값을 넣으면 slot 내부에서 랜더링된다.

slot은 1개 이상도 작성할 수 있다.

 

// 자식.vue
<template>
  <div>
    <h1><slot name="header" /></h1>
    <main>
      <slot name="contents" />
    </main>
  </div>
</template>
<script setup></script>

 

// 부모.vue
<template>
  <Child>
  	<!-- 자식요소의 slot의 name으로 넣은 값을 넣어준다. -->
    <template v-slot:header> header </template>
    <template #contents> <!-- "v-slot:" 을 줄여서 "#"으로 작성할 수 있다. -->
      <p>내용....</p>
    </template>
  </Child>
</template>
<script setup>
import Child from '~/components/common/Child.vue';
</script>

 

 

✨ slots에 데이터 전달(자식요소에 있는 data를 부모에 전달)

 

다음은 특정 요소를 클릭하면 아래에 작성한 요소가 나오는 tabMenu 처럼 동작하는 컴포넌트를 만들고자한다.

 

-  "자식.vue는" 디자인에 관여하지 않고 기능만 담당하게 하고자한다.

-  "닫기"버튼에 대한 evnet를 자식요소가 갖고, 부모에게 해당 evnet를 넘겨준다. (slot으로 event 전달)

-  열린 창 밖을 클릭해도 창이 닫힌다.

-  창을 닫을때 부모.vue에서 작성한 event가 동작하도록 한다.

 

// 부모.vue
<template>
  <!-- @click:outside는 card 밖을 클릭 했을 경우 창이 닫히며 동작할 evnet를 넣어준다. -->
  <Child @click:outside="closeEvent">
    <!-- button element -->
    <template #showBtn>
      <button class="openTab">클릭!</button>
    </template>
    <!-- tabMenu element -->
    <!-- evnet대신 원하는 이름으로 받아올 수 있다. -->
    <!-- 혹은 구조분해할당으로 받아올 수 있다. ex) #child="{ close }" -->
    <template #child="event"> 
      <div class="select-date">
        모달 내용....
        <div class="btn-wrap">
          <!-- 자식.vue에서 받은 #child="event"를 부모.vue에서 사용한다. -->
          <v-btn variant="outlined" @click="cancel(event.close)">닫기</v-btn>
        </div>
      </div>
    </template>
  </Child>
</template>

<script setup>
import Child from '~/components/common/Child.vue';

const closeEvent = (e) => {
  console.log(e);
  console.log('닫을때 발생하는 event');
};

const cancel = (close) => {
  closeEvent();
  close();
};
</script>

<style lang="scss" scoped>
.openTab {
  background: pink;
  border: 1px solid #000;
  padding: 5px;
  border-radius: 5px;
}
.select-date {
  padding: 20px 15px;
  width: 500px;
  border: 1px solid #000;
  .preview {
    text-align: center;
    font-weight: bold;
  }
  hr {
    background: #eee;
    height: 1px;
    border: 0;
    margin: 20px 0;
  }
  .btn-wrap {
    display: flex;
    justify-content: end;
    gap: 10px;
  }
}
</style>

 

// 자식.vue
<template>
  <div ref="$tabMenu" class="tab-menu">
    <div style="display: inline-block" @click="toggleTab">
      <slot name="showBtn"></slot>
    </div>
    <div v-if="isShowChild" class="tab-child">
      <!-- close라는 이름으로 toggleTab을 부모로 넘겨준다. -->
      <slot :close="toggleTab" name="child"></slot>
    </div>
  </div>
</template>

<script setup>
const $tabMenu = ref(null);
const emit = defineEmits(['click:outside']);

const isShowChild = ref(false); // toggle
const toggleTab = () => {
  isShowChild.value = !isShowChild.value;
};

const externalClickEvent = (e) => {
  // $tabMenu외에 다른 요소를 클릭했을 때만 실행된다.
  if ($tabMenu.value !== e.target.closest('.tab-menu')) {
    isShowChild.value = false;
    emit('click:outside', e);
  }
};

// .tab-menu 가 열려있을 경우, 화면전체에 click event를 걸어준다.
watch(isShowChild, () => {
  if (isShowChild.value) {
    document.addEventListener('click', externalClickEvent);
  }
});
</script>

<style scoped>
// style은 위치만 잡아주었다.
.tab-menu {
  position: relative;
}
.tab-child {
  position: absolute;
}
</style>

 

"자식.vue"에는 기능만 넣어두었기 해당 기능에 대한 코드를 "부모.vue" 에서는 간단한 코드만으로 해당 기능을 사용할 수 있다.

 

 

 

728x90

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading