Vuetify의 소소한 문제들
들어가며
Vuetify
는 Vue
개발에 아주 유용한 style library이다. Vue를 전제로 개발되었기 때문에 호환성도 무척 높을 뿐만 아니라 유저들이 원하는 대부분의 기능들이 구현되어 있다. 원하는 예제를 잘 골라 copy-and-paste만 잘 해도 그럴싸한 외형을 만들 수 있다.
그러나 기능을 깊이 사용하려면 Vuetify
뿐만 아니라 Vue
까지 잘 이해하고 있어야 한다. 게다가 공식문서가 매우 충실한 편임에도 불구하고 사용자가 알기 어려운 문제들도 있다.
여기에서는 개발 중에 만난 몇가지 문제들과 찾아낸 해결책을 기록해 둔다.
v-autocomplete
먼저 input에서 자동완성을 지원해 주는 v-autocomplete
directive이다. 테그 등을 입력할 때 매우 유용하다. 그러나 기능이 세세하게 구비되어 있는 만큼 API를 잘 이용하기까지는 시행착오가 불가피 했다.
items가 객체인 경우
자동완성은 전체 items에서는 입력값에 부합되는 subset items를 보여주는 기능이다. items가 아래와 같이 객체라면 어떻게 해야 할까? input에 텍스트로 표시되어야 하는 object 값의 key를 item-text
로 알려주고(“Set property of items’s text value”), 실제로 입력되는 값에 해당하는 object 값의 key를 item-value
로 지정해 주어야 한다. 하지만 이렇게만 하면, 입력 결과가 item-value
로 지정한 값, 즉 여기에서는 string 값을 가지게 된다. 입력 결과를 object로 하기 위해서는 return-object
directive를 반드시 넣어 주어야 한다.
<script>
export default {
data(){
return people: [
{ name: 'Sandra Adams', group: 'Group 1' },
{ name: 'Ali Connors', group: 'Group 1' },
{ name: 'Trevor Hansen', group: 'Group 1' },
{ name: 'Tucker Smith', group: 'Group 1' },
]
}
/* ... */
}
</script>
<v-autocomplete
:items="people"
item-text="name"
item-value="name"
v-model="friends"
return-object
></v-autocomplete>
tag를 선택한 뒤에 사용자 입력값 지우기
v-autocomplete
에서 여러개의 tag를 선택할 수 있는 multiple
directive를 사용하면, tag를 선택한 뒤에도 사용자가 입력한 텍스트가 그대로 살아있게 된다. 따라서 다음과 같이 input
event를 이용하여 search-input
값을 초기화 시켜줄 필요가 있다. ref
<v-autocomplete
:items="people"
item-text="name"
item-value="name"
v-model="friends"
return-object
multiple
:search-input.sync="userInput"
@input="userInput=null"
></v-autocomplete>
tag를 선택한 뒤에 menu slot 닫기
v-autocomplete
에서 여러개의 tag를 선택할 수 있는 multiple
directive를 사용하면, tag를 선택한 뒤에도 menu slot이 사라지지 않고 열려 있는 채로 다음 입력을 기다리게 된다. 데스크톱 환경에서는 문제가 없지만, 모바일 기기에서는 slot이 화면을 가득 채우기 때문에 입력이 불편해 질 수 있다. 따라서 하나의 값을 입력한 뒤에 slot을 닫을 필요가 생긴다.
slot을 닫기 위해서는 아래와 같은 menu-props
directive를 넣어 주면 된다.
<v-autocomplete
:items="people"
item-text="name"
item-value="name"
v-model="friends"
return-object
multiple
menu-props="{'closeOnContentClick': true}"
></v-autocomplete>
이렇게 하면 이론적으로 잘 동작해야 하지만, 기기에 따라 브라우저에 따라 잘 동작하지 않는 경우도 있었다. 그래서 실재 개발에서는 편법을 이용하였다. menu는 focus 되면 열리고 blur 되면 닫히게 되어 있다. 따라서 아래와 같이 입력 후에 blur 처리를 해 주고 조금 뒤에 focus를 주면 더 자연스럽게 이 문제를 해결할 수 있다.
<template>
<div>
<v-autocomplete
ref="autoinput"
:items="people"
item-text="name"
item-value="name"
v-model="friends"
return-object
multiple
@input="inputChanged"
></v-autocomplete>
<!-- ... -->
</div>
<template>
<script>
export default {
// ...
methods: {
inputChanged() {
//↓ For clear v-menu slot
this.$refs.autoinput.blur()
setTimeout(() => {
this.$refs.autoinput.focus()
}, 500)
},
},
}
</script></template
></template
>
사용자 입력이 완료되기를 기다리기
v-autocomplete
는 사용자가 키보드로 입력을 할 때마다 items을 필터링한다. 한글의 경우 ㅎ
, 학ㄱ
등을 넣어도 필터링이 되는데, 대부분의 경우 불필요한 로딩이다. 더군다나 items를 remote server에서 가져온다고 했을 때는 네트워크 로딩까지 겹치게 된다.
이런 경우에는 사용자가 입력을 마칠 때 까지 기다렸다가 필터링 하는 편이 유리히다. 0.5초 정도 입력이 없는 것을 입력이 완료되었다고 본다면 다음과 같이 하면 된다. ref
<template>
<div>
<v-autocomplete
:items="people"
item-text="name"
item-value="name"
v-model="friends"
return-object
multiple
@input="inputChanged"
:search-input.sync="userInput"
></v-autocomplete>
<!-- ... -->
</div>
</template>
<script>
export default {
data() {
return {
people: [
/* ... */
],
userInput: null,
}
},
props: ["itemData"],
watch: {
userInput(val) {
if (!val) {
return
}
this.fetchEntriesDebounced()
},
},
methods: {
fetchEntriesDebounced() {
// cancel pending call
clearTimeout(this._timerId)
// delay new call 500ms
this._timerId = setTimeout(() => {
// maybe : this.fetch_data()
this.people = this.itemData ? this.itemData : []
}, 500)
},
},
}
</script>
v-lazy
한 페이지에 표시해야 할 데이터가 많은 경우, 현재 화면에 나타낼 결과만 보여주는 것이 효과적이다. lazy
전략이다. Vuetify에서는 이를 위해 v-lazy
directive를 제공한다. 하지만 사용 방법은 조금 어렵다. ref
card
를 화면에 뿌려주기 위해 v-lazy
를 사용한다고 하였을 때,
v-sheet
나v-responsive
로v-lazy
를 감싸야 한다.- 대략적인
card
의 높이를 알고v-sheet
나v-responsive
에min-height
attribute를 통해 알려주어야 한다. - 각 객체 (여기서는 card)에
isActive
를 설정해 주어한다. 물론 객체에isActive
가 있어야 하는 것은 아니다.v-lazy
에서 참조용으로 선언만 해 주면 된다.
<template>
<v-row class="fill-height overflow-y-auto" dense>
<v-col
cols="12"
lg="4"
md="6"
v-for="(item, idx) in items"
:key="idx"
no-gutters
>
<v-sheet min-height="100" class="fill-height" color="transparent">
<v-lazy
class="fill-height"
v-model="item.isActivate"
:options="{threshold: .5}"
>
<v-card class="fill-height" rounded hover>
<div class="d-flex flex-no-wrap justify-space-between">
<v-card-title></v-card-title>
<v-card-subtitle
></v-card-subtitle
>
<v-card-text></v-card-text>
</div>
</v-card>
</v-lazy>
</v-sheet>
</v-col>
</v-row>
</template>
<script>
export default {
data() {
return {
items: [
{ title: "title1", subtitle: "subtitle1", text: "text1" },
{ title: "title2", subtitle: "subtitle2", text: "text2" },
{ title: "title3", subtitle: "subtitle3", text: "text3" },
/* ... */
// item must not have "isActivate" key & value
],
}
},
}
</script>