Implementação de um Teclado Numérico Personalizado com Vue.js

Cenário de Implementação: Ao clicar em um campo de entrada, um teclado numérico personalizado é exibido. Após a digitação dos números e confirmação, o valor é exibido no campo. Se o campo já contiver um valor, este será pré-carregado no teclado.

Onde: o teclado numérico é um componente filho, e o campo de entrada utiliza este componente compartilhado.

Principais Desafios:

  1. Valores passados via props para o componente do teclado não podem ser modificados diretamente. É necessário utilizar propriedades computadas para essa finalidade.
  2. Ao compartilhar o teclado, garantir que os números não sejam misturados incorretamente. Utilizar v-for com a propriedade name e alterar os parâmetros durante o clique para atingir o objetivo.

Código Completo:

index.vue

<template>
    <div class="container">
        <div v-for="(valor, nome) em listaValores" :key="nome">
            <div class="area-texto" v-if="nome=='CampoX'">
                Valor Padrão Positivo: <input  :value="valor"  @click="mostrarTeclado(nome, '0')">
            </div>
            <div class="area-texto" v-if="nome=='CampoY'">
                Valor Padrão Negativo: <input  :value="valor" @click="mostrarTeclado(nome, '1')">
            </div>
        </div>
        <TecladoNumerico :visivel="estaVisivel" :campoAtivo="campoAtivo" tamanhoFonte="20px" :ehNegativo="ehNegativo"  :valorNumerico="valorNumerico" @valorDigitado="receberValor"></TecladoNumerico>
    </div>
</template>

<script>
    import TecladoNumerico from "../Componentes/TecladoNumerico.vue";
    export default {
        data() {
            return {
                titulo: 'Implementação Vue',
                estaVisivel: false, // controla a exibição do teclado
                campoAtivo: '',
                ehNegativo: '0', // indica se é negativo (1 para negativo)
                valorNumerico: '',
                listaValores: {"CampoX": "","CampoY": ""},
            }
        },
        components:{
            TecladoNumerico
        },
        methods: {
            receberValor(valor, nomeCampo) {
                this.listaValores[this.campoAtivo] = valor;
                this.estaVisivel = false;
            },
            mostrarTeclado(nome, indicadorNegativo){
                this.estaVisivel = true;
                this.campoAtivo = nome;
                this.valorNumerico = this.listaValores[this.campoAtivo];
                this.ehNegativo = indicadorNegativo;
            },    
        }
    }
</script>

<style>
    .container {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    .area-texto {
        display: flex;
        justify-content: center;
        margin: 10px 0;
    }

    input {
        padding: 8px;
        margin-left: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
    }
</style>

TecladoNumerico.vue

<template>
    <div v-if="estaVisivel">
        <div class="fundo-transparente">
            <div class="visor">{{ valorFormatado }}</div>
            <div class="botoes">
                <div class="botao" @click="processarEntrada('1')">1</div>
                <div class="botao" @click="processarEntrada('2')">2</div>
                <div class="botao" @click="processarEntrada('3')">3</div>
                
                <div class="botao" @click="processarEntrada('limpar')">Limpar</div>
                <div class="botao" @click="processarEntrada('4')">4</div>
                <div class="botao" @click="processarEntrada('5')">5</div>
                <div class="botao" @click="processarEntrada('6')">6</div>
                
                <div class="botao" @click="processarEntrada('apagar')">Apagar</div>
                <div class="botao" @click="processarEntrada('7')">7</div>
                <div class="botao" @click="processarEntrada('8')">8</div>
                <div class="botao" @click="processarEntrada('9')">9</div>
                
                <div class="botao" @click="processarEntrada('.')">.</div>
                <div class="botao" @click="processarEntrada('-')">-</div>
                <div class="botao" @click="processarEntrada('0')">0</div>
                <div class="botao" @click="processarEntrada('+')">+</div>
                
                <div class="botao botao-azul" @click="processarEntrada('confirmar')">OK</div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data (){
            return {
                entradaAtual: '',
                primeiraRenderizacao: true
            }
        },
        props:{
            estaVisivel:{
                type: Boolean,
                default: false
            },
            tamanhoFonte: {
                type: String,
                default: "20px"
            },
            ehNegativo: {
                type: String,
                default: "0"
            },
            campoAtivo:{
                type: String,
                default: ""
            },
            valorNumerico:{
                type: String,
                default: ""
            }
        },
        computed: {
            valorFormatado () {
                if (this.primeiraRenderizacao) {
                    this.entradaAtual = this.valorNumerico;
                }
                return this.entradaAtual;
            }
        },
        methods: {
            processarEntrada(acao) {
                this.primeiraRenderizacao = false;
                
                if (acao == 'confirmar') {
                    // Confirmação
                    if ((this.entradaAtual.length == 1 && (this.entradaAtual[0] == '+' || this.entradaAtual[0] == '-')) || 
                        this.entradaAtual.substring(this.entradaAtual.length-1) == ".") {
                        return;
                    }
                    this.$emit('valorDigitado', this.entradaAtual, this.campoAtivo);
                    this.primeiraRenderizacao = true;
                } else if (acao == 'apagar') {
                    // Excluir último caractere
                    this.entradaAtual = this.entradaAtual.substring(0, this.entradaAtual.length - 1);
                } else if (acao == '+' || acao == '-') {
                    // Sinal positivo/negativo
                    if (this.entradaAtual[0] != '+' && this.entradaAtual[0] != '-') {
                        this.entradaAtual = acao + this.entradaAtual;
                    } else {
                        this.entradaAtual = acao + this.entradaAtual.substring(1);
                    }
                } else if (acao == '.') {
                    // Ponto decimal
                    let contemPonto = false;
                    for (let i = 0; i < this.entradaAtual.length; i++) {
                        if (this.entradaAtual[i] == '.') {
                            contemPonto = true;
                            break;
                        }
                    }
                    
                    if (this.entradaAtual.length == 0 || 
                        (this.entradaAtual.length == 1 && (this.entradaAtual[0] == '+' || this.entradaAtual[0] == '-'))) {
                        // Não permite ponto se estiver vazio ou após sinal
                    } else if (!contemPonto) {
                        this.entradaAtual += acao;
                    }
                } else if (acao == 'limpar') {
                    // Limpar tudo
                    this.entradaAtual = '';
                } else {
                    // Números
                    if (acao == "0" && this.entradaAtual.length == 0) {
                        // Se começar com 0, adiciona ponto
                        this.entradaAtual = "0.";
                    } else {
                        this.entradaAtual += acao;
                    }

                    if (this.ehNegativo == '1' && this.entradaAtual[0] != "+" && this.entradaAtual[0] != "-") {
                        // Define como negativo se configurado
                        this.entradaAtual = "-" + this.entradaAtual;
                    }
                }
            },
        }
    }
</script>

<style>
/* Estilo do teclado numérico */
.fundo-transparente {
    width:100%;
    height: 305px;
    position: fixed;
    bottom: 0;
    left: 0;
    margin: auto;
    z-index: 999;
    background: #f7f7f7;
    border-radius: 6px;
}

.botoes {
    padding: 0 15px;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
}

.botao {
    width: 23%;
    height: 50px;
    line-height: 50px;
    text-align: center;
    font-size: 20px;
    background: #fff;
    margin-bottom: 10px;
    border-radius: 6px;
    border: solid 1px #eee;
    cursor: pointer;
    transition: background-color 0.2s;
}

.botao:hover {
    background: #efefef;
}

.visor {
    width: 90%;
    height: 40px;
    line-height: 40px;
    text-align: right;
    font-size: 25px;
    padding: 5px 15px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.botao-azul {
    color: #fff;
    background: #0078ff;
}
</style>

Atualização 2023: Utilizando o modificador .sync

O método .sync também pode ser utilizado para modificar props, como demonstrado abaixo:

Componente pai:

<TecladoNumerico :valorNumerico.sync="valorNumerico"></TecladoNumerico>

Componente filho:

// As props permanecem as mesmas
props:{
    valorNumerico:{
        type: String,
        default: ""
    }
},

// No evento de clique
this.$emit('update:valorNumerico', novoValor);

Tags: Vue.js componentes teclado numérico propriedades computadas two-way data binding

Publicado em 6-30 06:48