아래 에제는 modal 창을 구현한 예제이다.
// App.vue
<template>
<Modal width="300px">
<template #activator>
<button>On Modal!</button>
</template>
<h3>App.vue</h3>
</Modal>
<Hello />
</template>
<script>
import Hello from "~/components/Hello";
export default {
components: {
Hello,
},
data() {
return {
msg: "Hello Vue!",
};
},
};
</script>
// Modal.vue
<template>
<div @click="onModal">
<slot name="activator"></slot>
</div>
<template v-if="isShow">
<div
class="modal"
@click="offModal">
<div
:style="{ width: `${parseInt(width, 10)}px`}"
class="modal__inner"
@click.stop>
<slot></slot>
</div>
</div>
</template>
</template>
<script>
export default{
props:{
width: {
type: [String, Number],
default: 400
}
},
data() {
return {
isShow: false
};
},
methods: {
onModal(){
this.isShow = true;
},
offModal(){
this.isShow = false;
}
}
};
</script>
<style lang="scss" scoped>
.modal {
background-color: rgba(black, .5);
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
&__inner{
background-color: white;
box-sizing: border-box;
padding: 20px;
}
}</style>
하지만 현재 modal을 fixed로 사용했지만 만약 상위 요소에 transform이나 filter 등의 스타일 속성이 있으면 제대로 출력이 되지 않는다.
이럴 때 사용할 수 있는 VueJS의 기능이 Teleport이다.
컴포넌트 내부의 데이터를 온전하게 사용하면서 원하는 위치에서 <teleport>
로 감싼 요소를 출력할 수 있다.
to 속성으로 body요소 내부로 순간이동 시켰다.
예제 최종 코드
textarea에 자동으로 focus를 주기위해 modal 온오프 변수를 상위 컴포넌트에서 관리
// App.vue
<template>
<div style="transform: scale(1);">
<Modal
v-model="isShow"
width="300px">
<template #activator>
<button>On Modal!</button>
</template>
<h3>App.vue</h3>
</Modal>
<Hello />
</div>
</template>
<script>
import Hello from "~/components/Hello";
export default {
components: {
Hello,
},
data() {
return {
isShow: false,
msg: "Hello Vue!",
};
},
};
</script>
// Hello.vue
<template>
<Modal
v-model="isShow"
closeable>
<template #activator>
<h1>Hello</h1>
</template>
<h3>Hello.vue</h3>
<textarea
ref="editor"
v-model="msg"></textarea>
<button @click="submit">
Submit!
</button>
</Modal>
</template>
<script>
export default {
data(){
return {
msg: "Pleas enter the text.",
isShow: false
};
},
watch: {
isShow(newValue) {
if(newValue) {
this.$nextTick(() => {
this.$refs.editor.focus();
});
}
}
},
methods: {
submit(){
console.log(this.msg);
}
}
};
</script>
<style scoped lang="scss">
$color: red;
h1 {
color: $color;
}
textarea{
width:100%;
height:100px;
box-sizing: border-box;
}
</style>
// Modal.vue
<template>
<div @click="onModal">
<slot name="activator"></slot>
</div>
<teleport to="body">
<template v-if="modelValue">
<div
class="modal"
@click="offModal">
<div
:style="{ width: `${parseInt(width, 10)}px`}"
class="modal__inner"
@click.stop>
<button
v-if="closeable"
class="close"
@click="offModal">
x
</button>
<slot></slot>
</div>
</div>
</template>
</teleport>
</template>
<script>
export default{
props:{
modelValue: {
type:Boolean,
default: false
},
width: {
type: [String, Number],
default: 400
},
closeable: {
type:Boolean,
default:false
}
},
emits:["update:modelValue"],
watch: {
modelValue(newValue){
if(newValue){
window.addEventListener("keyup", this.keyupHandler);
} else{
window.removeEventListener("keyup", this.keyupHandler);
}
}
},
methods: {
keyupHandler(event){
if(event.key === "Escape"){
this.offModal();
}
},
onModal(){
this.$emit("update:modelValue", true);
},
offModal(){
this.$emit("update:modelValue", false);
}
}
};
</script>
<style lang="scss" scoped>
.modal {
background-color: rgba(black, .5);
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
&__inner{
background-color: white;
box-sizing: border-box;
border-radius: 6px;
box-shadow: 0 10px 10px rgba(black, .2);
padding: 20px;
button.close{
float: right
}
}
}
</style>