Técnico

Encapsulando Componentes VueJS e adicionando novas funcionalidades

É muito comum utilizar bibliotecas de componentes para facilitar o nosso trabalho do dia a dia, elas costumam ser extremamente dinâmicas, com um layout padronizado e testadas pela comunidade, mas o que ocorre quando você precisa de uma funcionalidade que não foi implementada? Esse foi um problema que aconteceu comigo recentemente e creio que possa já ter ocorrido com você também.

Em um projeto estávamos utilizando a biblioteca Vuetify como base do nosso layout de formulários, e em vários locais na aplicação era necessário utilizar o input de valores monetários, na época, o único plugin que existia para isso era o v-money, ele fornece um componente de input que funciona perfeitamente, e uma diretiva que eu queria utilizar, já que precisava manter o v-text-field que havia sido aplicado no app inteiro. 

Foi aí que surgiu o problema. Ao realizar o teste dele como diretiva, por algum motivo interno ele converteu o meu model para String, me forçando a realizar parse do valor para decimal em todos os lugares, gerando muitos pontos de possíveis erros e manutenção futura.

A solução? Criar o nosso próprio componente de currency, encapsulando o v-text-field em um componente chamado currency-field. A ideia era bem simples: capturar qualquer input gerado no v-text-field, formatar ele no currency padrão do sistema ($0.000,00) e devolver o valor em decimal no model do componente. Para não ter que lidar com a parte de formatação, utilizei a lib Numeral.js. Vamos visualizar o código utilizado para isso:

<template>
  <v-text-field ref="currencyField" v-model="currencyFieldValue">
  </v-text-field>
</template>

<script>
import numeral from 'numeral'

export default {
  name: 'CurrencyField',
  props: ['value', 'currencyPrefix'],
  data() {
    return {
      fieldValue: null,
      prefix: '$',
    }
  },
  created() {
    this.fieldValue = this.value
    this.prefix = this.currencyPrefix || '$'
  },
  computed: {
    currencyFieldValue: {
      get() {
        return this.fieldValue == null ?
          "" :
          numeral(this.fieldValue).format(`${this.prefix}0,0.00`)
      },
      set(newValue) {
        this.currencyFieldValueChanged(newValue)
      }
    }
  },
  watch: {
    value(newValue) {
      if(newValue == this.fieldValue) return

      this.fieldValue = newValue
      this.updateViewValue(newValue)
    },
  },
  methods: {
    currencyFieldValueChanged(newVal) {
      if(newVal == this.currencyFieldValue) return
      if (!newVal.length) {
        this.updateModelValue(null)
        return
      }

      let newParsedValue = parseFloat(newVal.replace(/\D/gi, '')) / 100
      this.updateModelValue(newParsedValue)
    },
    updateViewValue() {
      let el = this.$refs.currencyField.$el.getElementsByTagName('input')[0]
      el.value = this.currencyFieldValue
      el.dispatchEvent(this.createEvent('input'))
    },
    updateModelValue(modelValue) {
      this.fieldValue = modelValue
      this.$emit('input', modelValue)
      this.updateViewValue()
    },
    createEvent (name) {
      var evt = document.createEvent('Event')
      evt.initEvent(name, true, true)
      return evt
    }
  }
}
</script>

Os pontos de atenção neste código são 2: a entrada e saída de valores no componente, e a entrada e saída de valores no nosso v-text-field que estamos encapsulando.  No text-field utilizamos uma variável computed com setter e getter como v-model, no get aplicamos a nossa máscara para para mostrar o dado formatado e no set pegamos o input do usuário. 

Já para a entrada e saída de valores do nosso componente, nós damos watch na prop value caso o valor tenho sido alterado externamente, e executamos this.$emit(‘input’, modelValue) para informar quem estiver escutando o v-model ou o @input do nosso currency-field.Com isso, nosso componente está pronto, porém ficamos com alguns problemas: e todas as props que tenho no v-text-field? E se eu quiser fazer uma validação com a prop rule, definir um label ou placeholder? É aí que as novas funcionalidades do ES6 vem a calhar. Utilizando o Spread Operator, podemos linkar todas as props e attrs passadas para o nosso componente diretamente para o v-text-field.

<template>
  <v-text-field v-bind="{...$props, ...$attrs}" ref="currencyField" v-model="currencyFieldValue">
  </v-text-field>
</template>

Espero que tenham gostado da solução para esse problema, o componente ainda possui muito espaço para melhorias, uma forma de configuração global ou registrar novos formatters do numeral.js ou até mesmo transformar ele em um proxy para o numeral.js e permitir que o usuário passe a máscara na qual ele quer formatar os números. 

Quer saber mais sobre esse e outros assuntos e soluções em tecnologia? Continue acompanhando o blog da Redspark e siga nossas redes sociais! 

Autor(a)

Renato Massi Pereira

Deixe um comentário

O seu endereço de e-mail não será publicado.