Componente de Input personalizado com Vue
Tutorial para criar componentes de input customizados com Vue
A maioria de nós já enfrentou a seguinte situação: construir um componente de input personalizado. Existem várias razões para isso, mas em geral, o input tem estilos personalizados e devemos ser capazes de reutilizá-lo em nossas aplicações.
Embora possa parecer simples, existem alguns truques e de vez em quando precisamos analisar a documentação (nesse caso, do Vue) para verificar detalhes de implementação. Fica um pouco mais complicado se você não estiver familiarizado com alguns conceitos importantes do Vue.
No mês passado, fevereiro de 2021, aconteceu novamente. Sempre que possível, tento ajudar as pessoas de um grupo do Vue no Slack e esta pergunta surgiu novamente. Não exatamente esta questão, mas uma pessoa estava com problemas para construir um componente de input personalizado. O problema estava relacionado ao entendimento de alguns conceitos.
Para consolidar esse conhecimento para mim mesmo e usá-lo como algum tipo de documentação para outras pessoas, decidi compilar o processo de escrever um input personalizado.
Índice
v-model
einput
O componente de input personalizado errado
O componente de input personalizado feliz
Adicionando validação
Combinando computed e v-model
Extra: a propriedade
model
E agora?
v-model e <input>
Depois de começar a construir formulários com o Vue, aprendemos a diretiva v-model
.
A diretiva faz um grande trabalho para nós: vincula um valor a um input. Isso significa que sempre que alterarmos o valor do input, a variável também será atualizada.
Os documentos oficiais explicam como funciona: https://vuejs.org/v2/guide/forms.html
Resumindo, podemos seguir o seguinte modelo:
<template> | |
<label> | |
Username | |
<input type="text" name="username" v-model="username"> | |
</label> | |
</template> | |
<script> | |
export default { | |
name: 'UsernameInput', | |
data() { | |
return { | |
username: 'Initial value', | |
}; | |
}, | |
} | |
</script> |
Teremos um input que tem Initial value
como valor inicial e os dados de username
serão atualizados automaticamente assim que alterarmos o valor do input.
O problema com o componente acima é que não podemos reutilizá-lo. Imagine que temos uma página onde precisamos do nome de usuário (username
) e do e-mail. O componente acima não vai saber lidar com o caso do e-mail porque os dados estão dentro do próprio componente, não em outro lugar (como o componente pai, por exemplo) . É aí que os componentes de input customizados brilham.
O componente de input personalizado errado
Bem, mas por que estou mostrando este exemplo? A resposta é: esta é a primeira abordagem que a maioria de nós tentará.
Vamos ver como vamos usar nosso componente de input personalizado:
<!-- App.vue --> | |
<template> | |
<custom-input :label="label" v-model="model" /> | |
</template> | |
<script> | |
import CustomInput from './components/CustomInput.ue'; | |
export default { | |
name: 'App', | |
components: { CustomInput }, | |
data() { | |
return { | |
label: 'Username', | |
model: '', | |
}; | |
}, | |
} | |
</script> |
Neste caso, o input personalizado espera um label
e um v-model
e será semelhante ao componente abaixo:
<!-- CustomInput.vue --> | |
<template> | |
<label> | |
{{ label }} | |
<input type="text" :name="name" v-model="value" /> | |
</label> | |
</template> | |
<script> | |
export default { | |
name: 'CustomInput', | |
props: { | |
label: { | |
type: String, | |
required: true, | |
}, | |
value: { | |
type: String, | |
required: true, | |
}, | |
}, | |
computed: { | |
name() { | |
return this.label.toLowerCase(); | |
}, | |
}, | |
} | |
</script> |
Primeiro, é esperado um label
como propriedade e logo após calcula o name
em cima disso (também pode ser uma propriedade).
Em segundo lugar, é esperado a propriedade value
e acontece o vínculo ao input
por meio de v-model
. A razão por trás disso pode ser encontrada na documentação, mas, em resumo, quando usamos o v-model
em um componente personalizado, ele obterá o value
como uma propriedade que é o valor da variável do v-model
utilizado. Em nosso exemplo, será o valor do nosso modelo definido em App.vue
.
Se tentarmos o código acima, ele funcionará conforme o esperado, mas por que está errado? Se abrirmos o console, veremos algo assim:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
O aviso exibido é que estamos alterando uma propriedade. A maneira como o Vue funciona é: o componente filho tem propriedades que vieram do componente pai e o componente filho emite alterações para o componente pai. Usar o v-model
com a propriedade value
que obtivemos do componente pai viola a regra de imutabilidade.
Outra maneira de ver esse problema é reescrever o App.vue
assim:
<!-- App.vue --> | |
<template> | |
<custom-input :label="label" :value="model" /> | |
</template> | |
... |
A principal diferença está no uso de :value
no lugar de v-model
. Nesse caso, estamos apenas passando o model
para a propriedade value
. O exemplo ainda funciona e recebemos a mesma mensagem no console.
A próxima etapa é melhorar o exemplo acima e verificar se ele funciona conforme o esperado.
O componente de input personalizado feliz
O componente de input personalizado feliz não altera seu prop, mas emite as alterações para o componente pai.
A documentação possui exatamente esse exemplo, mas iremos um pouco mais além aqui. Se seguirmos a documentação, nosso CustomInput
deve ser semelhante a este abaixo:
<!-- CustomInput.vue --> | |
<template> | |
<label> | |
{{ label }} | |
<input type="text" :name="name" :value="value" @input="$emit('input', $event.target.value)" /> | |
</label> | |
</template> | |
<script> | |
export default { | |
name: 'CustomInput', | |
props: { | |
label: { | |
type: String, | |
required: true, | |
}, | |
value: { | |
type: String, | |
required: true, | |
}, | |
}, | |
computed: { | |
name() { | |
return this.label.toLowerCase(); | |
}, | |
}, | |
} | |
</script> |
Isso é o suficiente para fazer funcionar. Podemos até mesmo testá-lo em relação ao App.vue
, aquele que estava usando o v-model
, onde tudo funciona como esperado, e também aquele que usa somente :value
, onde não funciona quando paramos de alterar a propriedade.
Adicionando validação
No caso de precisarmos fazer algo quando o dado muda, por exemplo verificar se ele está vazio e mostrar alguma mensagem de erro, é necessário extrair o emit. Teremos as seguintes alterações em nosso componente:
<!-- CustomInput.vue --> | |
<template> | |
... | |
<input type="text" :name="name" :value="value" @input="onInput" /> | |
... | |
</template> | |
<script> | |
... | |
methods: { | |
onInput(event) { | |
this.$emit('input', event.target.value); | |
} | |
} | |
... | |
</script> |
Agora adicionamos a validação para dados vazios:
<!-- CustomInput.vue --> | |
<template> | |
... | |
<p v-if="error">{{ error }}</p> | |
... | |
</template> | |
<script> | |
... | |
data() { | |
return { | |
error: '', | |
}; | |
}, | |
... | |
onInput(event) { | |
const value = event.target.value; | |
if (!value) { | |
this.error = 'Value should not be empty'; | |
} | |
this.$emit('input', event.target.value) | |
} | |
... | |
</script> |
Isso meio que funciona, primeiro não é exibido nenhum erro e se digitarmos e excluirmos, será exibida a mensagem de erro. O problema é que a mensagem de erro nunca desaparece. Para corrigir isso, precisamos adicionar um watcher ao valor da propriedade e limpar a mensagem de erro sempre que ela for atualizada.
<!-- CustomInput.vue --> | |
... | |
<script> | |
... | |
watch: { | |
value: { | |
handler(value) { | |
if (value) { | |
this.error = ''; | |
} | |
}, | |
}, | |
}, | |
... | |
</script> |
Poderíamos ter um resultado semelhante adicionando um else
dentro de onInput
. Usar o watcher nos permite validar antes que o usuário atualize o valor do input, se desejável.
Se adicionarmos mais coisas, provavelmente iremos expandir este componente ainda mais e as coisas estarão espalhadas por todo o bloco <script>
. Para agrupar as coisas um pouco, podemos tentar uma abordagem diferente: usar computed
junto com o v-model
.
Combinando computed e v-model
Ao invés de adicionar um listener ao input
e depois emiti-lo novamente, podemos aproveitar o poder do v-model
e do computed
. É o mais próximo que podemos chegar da abordagem errada, mas ainda assim acertar 😅
Vamos reescrever nosso componente:
<!-- CustomInput.vue --> | |
<template> | |
... | |
<input type="text" :name="name" v-model="model" /> | |
... | |
</template> | |
<script> | |
... | |
computed: { | |
... | |
model: { | |
get() { | |
return this.value; | |
}, | |
set(value) { | |
this.$emit('input', value); | |
}, | |
}, | |
}, | |
... | |
</script> |
Podemos nos livrar do método onInput
e também do watcher, pois podemos lidar com tudo nas funções get/set
da propriedade computed.
Uma coisa legal que podemos conseguir com isso é o uso de modificadores, como .trim/number
que precisariam ser escritos manualmente antes.
Esta é uma boa abordagem para componentes de input simples. As coisas podem ficar um pouco mais complexas e esta abordagem não cumpre todos os casos de uso, se for esse o caso, precisamos ir para binding de valor e listeners de eventos. Um bom exemplo é se você quiser suportar o modificador .lazy
no componente pai. Nesse caso você precisará manualmente adicionar listeners ao input.
Extra: a propriedade model
A propriedade model permite que você personalize o comportamento do v-model
. Você pode especificar qual propriedade será mapeada, o padrão é value.
Também é possível customizar qual evento será emitido. O padrão nesse caso é input
ou change
quando .lazy
é usado.
Isso é especialmente útil se você deseja usar a propriedade value
para outra coisa, pois pode fazer mais sentido para um contexto específico, ou se apenas deseja tornar as coisas mais explícitas e renomear o value
para model
, por exemplo. Na maioria dos casos, podemos usá-lo para personalizar checkboxes/radios ao receber objetos como input.
E agora?
Depende de quão complexo seu input personalizado precisa ser:
O input foi criado para centralizar os estilos em um componente e sua API está praticamente em cima da API do Vue:
computed
+v-model
. Isso se encaixa muito bem em nosso exemplo, tem propriedades simples e nenhuma validação complexa.<!-- CustomInput.vue --> <template> <label> {{ label }} <input type="text" :name="name" v-model="model" /> </label> </template> <script> export default { name: 'CustomInput', props: { label: { type: String, required: true, }, value: { type: String, required: true, }, }, computed: { name() { return this.label.toLowerCase(); }, model: { get() { return this.value; }, set(value) { this.$emit('input', value); }, }, }, } </script> Todo o resto (o que significa que você precisa ajustar muito a configuração anterior para suportar o que você precisa): listeners, watchers e o que mais você precisar. Ele pode ter vários estados (pense na validação assíncrona em que um estado de carregamento pode ser útil) ou você deseja oferecer suporte ao modificador
.lazy
do componente pai, são bons exemplos para evitar a primeira abordagem.<!-- CustomInput.vue --> <template> <label> {{ label }} <input type="text" :name="name" :value="value" @input="onInput" @change="onChange" /> </label> </template> <script> export default { name: 'CustomInput', props: { label: { type: String, required: true, }, value: { type: String, required: true, }, }, /* Can add validation here watch: { value: { handler(newValue, oldValue) { }, }, }, */ computed: { name() { return this.label.toLowerCase(); }, }, methods: { onInput(event) { // Can add validation here this.$emit('input', event.target.value); }, onChange(event) { // Supports .lazy // Can add validation here this.$emit('change', event.target.value); }, }, } </script> Este texto foi originalmente escrito por Vinicius Kiatkoski Neves e sua tradução para o português e publicação no site da BrazilJS foi previamente autorizada pelo autor.
Link para o post original em inglês: https://dev.to/viniciuskneves/vue-custom-input-bk8