그 동안 vuetify를 쓰며 봤던 slot!
예제만 대충 복사해서 쓰고 어떻게 동작하는지는 모르고 있었다.
그동안 귀찮아서 안 찾아보고 있었는데 확장성 있는 컴포넌트를 만들게되며 이제야 찾아보게 되었다.
slot외에도 props, emit 컴포넌트를 만들때 사용되는 문법들을 한번 정리 해보고자 한다.
앞으로 사용할 예제는 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은 부모요소가 자식요소에게 함수 또는 데이터를 전달해 줄 수 있다.
// 자식.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은 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>
위의 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는 부모에서 자식으로 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>
다음은 특정 요소를 클릭하면 아래에 작성한 요소가 나오는 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" 에서는 간단한 코드만으로 해당 기능을 사용할 수 있다.
[Nuxt3-Quasar] qCalendar를 이용한 DatePicker (0) | 2023.05.25 |
---|---|
[Nuxt3] i18n 과 Google Spread Sheet를 통한 다국어지원 자동화 (0) | 2023.05.23 |
[Nuxt3] 빌드방식 3가지(ssr,csr,generate) (0) | 2023.04.28 |
[VCalendar] V-Calendar을 이용한 날짜 input 창 (0) | 2023.04.28 |
[v-calendar] Nuxt3에서 V-calendar 세팅 (0) | 2023.03.17 |