Vuejs가 제공하는 유용한 props들이 있다. 가령 v-model
, @focus
등의 이벤트 인터페이스 들은 input
, textarea
등의 native component에 prop, attr으로 건내주는 것 만으로 기대되어지는(?) 작동을 잘 한다
<input v-model="myInputValue" />
<textarea
v-model="myTextAreaValue"
v-on:hover="handleHover"
rows="7"
cols="20"
/>
이렇게 native component들에 v-model, v-on:hover 등 props를 넣으면 잘 작동한다.
그런데 Vue로 컴포넌트들 개발하다 보면 native component처럼 동작하면 좋을 것 같은 컴포넌트를 만들고 싶을 때가 있다.
<my-customized-input v-model="myInputValue" />
<my-customized-textarea
v-model="myTextAreaValue"
v-on:hover="handleHover"
rows="7"
cols="20"
/>
이런식으로 말이다.
이렇게 내가 만든 component
가 transparent 하게 prop
들을 native component
로 전달해줘서 native component
처럼 쓸 수 있게 만든 component 를 보고 Transparent Wrapper Component
라고 한다.
그럼 이 Transparent Wrapper Component
를 어떻게 만들 것인가?
이 글의 저자는 3단계에 걸쳐서 Transparent Wrapper Component
를 만들었다.
// I want to use v-model like this
<my-customized-textarea v-model="myTextAreaValue" />
Vue가 제공하는 특별한 prop중 하나인 v-model을 value
라는 이름으로 data를 prop으로 넘겨주고, 그 data를 변화 시키는 함수를 input
event listner에 넘겨준 것과 동일 하다는 것은 익히 아는 사실이다.
<input v-model="someValue">
equals
<input
v-bind:value="someValue"
v-on:input="someValue = $event.target.value"
>
(v-model의 원리에 대해 궁금한 분은 여기를 참고)
그렇다면 내가 만들 component가 value
라는 이름의 props를 받고, input
이라는 이름의 event를 emit시켜주도록 만들면 v-model을 사용 할 수 있게 된다
// In javascript
export default {
name: 'MyTransparentWrapperComponent'
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
template: `
<textarea
:value="value"
@input="input"
>
</textarea>
`
};
// I want to use v-model, events like this
<my-customized-textarea
v-model='myTextAreaValue'
@blur="handleBlur"
@click="handleClick"
@focus="handleFocus"
@mouseenter="handleMouseenter"
@mouseleave="handleMouseleave"
.
.
.
/>
v-model을 사용할 수 있지만 @focus, @mouseenter 등등의 event handler들을 사용하지 못하는 상황이다. 이 또한 native component들 처럼 바로 props로 건내 주어서 사용할 수 있게 만들고 싶은 것이다.
그렇다면 생각해 낼 수 있는 모든 event handler들을 대응해 주어야 할까?
// In javascript, OMG... there is too many events!!
export default {
name: 'MyTransparentWrapperComponent'
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
blur() {
this.$emit('blur');
},
click(event) {
this.$emit('click', event.target.value);
},
focus() {
this.$emit('focus');
},
mouseenter() {
this.$emit('mouseenter');
},
mouseleave() {
this.$emit('mouseleave');
},
.
.
.
},
template: `
<textarea
:value="value"
@input="input"
@blur="blur"
@click="click"
@focus="focus"
@mouseenter="mouseenter"
@mouseleave="mouseleave"
.
.
.
>
</textarea>`
},
};
위의 코드 처럼 사용하는 모든 케이스의 event handler를 다 작성할 것인가? No way! 너무 손이 아픈 것이다!
우리에겐 Vue instance가 제공하는 $listeners
속성이 있다! $listeners
는 이 컴포넌트가 props으로 받는 모든 event가 객체형태로 저장되어있다. 그렇다면 이 $listeners
에 들어있는 (input event를 제외한) 모든 event들을 v-on
을 이용하여 그대로 전달해 주면 간단하게 event를 passing 할 수 있다
(이에 관해 더 궁금하시다면 여기를 참고하는 것이 좋다.)
// In javascript, wow! very simplified!
export default {
name: 'MyTransparentWrapperComponent'
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
computed: {
lisnters() {
const { input, ...listeners } = this.$listeners;
return listeners
},
},
template: `
<textarea
:value="value"
@input="input"
v-on="lisnters"
>
</textarea>
`
};
// I want to use v-model, events, attributes like this
<my-customized-textarea
v-model="myTextAreaValue"
@blur="handleBlur"
@click="handleClick"
@focus="handleFocus"
@mouseenter="handleMouseenter"
@mouseleave="handleMouseleave"
rows="7"
cols="20"
/>
이벤트들도 다 전달 시켰으니 마지막으로 attribute들도 전달 시켜 주고 싶다. textarea
tag를 예로 들면, rows라는 attribute는 이 element이 행 수를 결정 짓는다.
Vue instance의 속성 중 전달받은 attribute를 모아놓은 속성은 없을까? 물론 존재한다 ㅎㅎ.
Vue instance의 $attrs
속성에 전달받은 attr
이 다 들어있다!
이 $attrs
를 v-bind
를 통해 전달 시킬 수 있다.
// In javascript
export default {
name: 'MyTransparentWrapperComponent'
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
computed: {
lisnters() {
const { input, ...listeners } = this.$listeners;
return listeners
},
attrs() {
return this.$attrs
}
},
template: `
<textarea
:value="value"
@input="input"
v-on="lisnters"
v-bind="attrs"
>
</textarea>
`
};
짜잔! native component처럴 v-model을 사용할 수 있고 event interface와 attr을 그대로 내려 받는 Transparent Wrapper Component가 완성되었다!! 🎉🎉🎉
내가 만든 컴포넌트의 depth가 1단계가 아닐 수 있다. 위 예시를 depth가 2단계인 컴포넌트로 만들어 보겠다.
// In javascript
export default {
name: 'MyTransparentWrapperComponent'
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
computed: {
lisnters() {
const { input, ...listeners } = this.$listeners;
return listeners
},
attrs() {
return this.$attrs
}
},
template: `
<div class="root-element">
<h1>My Transparent Wrapper Component ^^</h1>
<textarea
:value="value"
@input="input"
v-on="lisnters"
v-bind="attrs"
>
</textarea>
</div>
`
};
// Use it like this
<my-transparent-wrapper-component
v-model="myTextAreaValue"
v-on:hover="handleHover"
rows="10"
cols="29"
>
</my-transparent-wrapper-component>
위의 코드 처럼 component에 attr을 넣어주면 아래 그림 처럼 자동적으로 컴포넌트의 root element에 attribute로 들어간다.
rows와 cols는 textarea에만 넣었는데 root element 에도 attribute로 들어간 이유는 Vue component옵션 중 inheritAttrs
옵션이 기본적으로 true
이기 때문이다.
따라서 이 옵션을 false 로 바꿔주면 root element 에 자동적으로 들어간 attribute 들이 없어진다.
// In javascript
export default {
name: 'MyTransparentWrapperComponent'
inheritAttrs: false // here is the option ^^@
props: ['value'],
methods: {
input(event) {
this.$emit('input', event.target.value);
},
},
computed: {
lisnters() {
const { input, ...listeners } = this.$listeners;
return listeners
},
attrs() {
return this.$attrs
}
},
template: `
<div class="root-element">
<h1>My Transparent Wrapper Component ^^</h1>
<textarea
:value="value"
@input="input"
v-on="lisnters"
v-bind="attrs"
>
</textarea>
</div>
`
};
// Use it like this
<my-transparent-wrapper-component
v-model="myTextAreaValue"
v-on:hover="handleHover"
rows="10"
cols="29"
>
</my-transparent-wrapper-component>
짜잔! root element에 자동적으로 들어가있던 attr인 rows와 cols들이 사라졌다 ^_^
모든 피드백 거대하게 감사합니다. :)
참고
https://zendev.com/2018/05/31/transparent-wrapper-components-in-vue.html
https://www.youtube.com/watch?v=7lpemgMhi0k
https://medium.com/@Dongmin_Jang/vuejs-%EC%88%A8%EA%B2%A8%EC%A7%84-vue-%ED%8C%A8%ED%84%B4%EB%93%A4-1ea3adc585ac
https://blog.woolta.com/categories/10/posts/139
이런 꿀팁이! 감사합니다.