Input Porcentagem - Angular
Trabalhar com input's é uma das primeiras lições que aprendemos quando trabalhamos no frontend. Neste artigo é apresentado como podemos criar um componente em Angular para o usuário digitar um valor percentual sem necessariamente ter um <input>.
Construção do Componente
Vamos tomar como base o código abaixo e evoluí-lo aos poucos:
import { Component } from '@angular/core';
@Component({
selector: 'app-input-porcentagem',
template: `
<div class="label">Label</div>
<div class="input">{{valor}}</div>
`,
styleUrls: ['./input-porcentagem.component.css']
})
export class InputPorcentagemComponent {
valor: number = 0;
}
Capturando a tecla pressionada com @HostListener
Um <div>
ou um <app-input-porcentagem>
por padrão não recebe foco, ou seja, se você clicar em um destes elementos na tela nada vai acontecer. Para permitir que um <div>
ganhe o foco podemos adicionar um atributo tabindex
conforme o exemplo a seguir:
<div tabindex="0"></div>
Agora que o <div>
pode receber o foco, podemos utilizar o evento onkeydown
para capturar os eventos do teclado quando este <div>
tiver foco:
Seguindo essa mesma lógica aplicada em cima de um <div>
, podemos aplicá-la em cima do <app-input-porcentagem>
. Então o que precisamos é de um tabindex
e um onkeydown
no <app-input-porcentagem>
. Para fazer isto vamos utilizar o @HostBinding
e @HostListener
respectivamente:
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-input-porcentagem',
template: `
<div class="label">Label</div>
<div class="input">{{valor}}</div>
`,
styleUrls: ['./input-porcentagem.component.css']
})
export class InputPorcentagemComponent {
valor: number = 0;
@HostBinding('attr.tabindex') tabindex = 0;
@HostListener('keydown', ['$event.key'])
hostKeydown(key: string) {
console.log('Tecla pressionada', key);
}
}
Agora o componente pode receber o foco e também sabemos o que está sendo digitado enquanto o componente tem o foco:
Desenvolvendo a lógica
Agora que temos um componente que pode receber o foco e já sabemos quais teclas são pressionadas enquanto o componente tem o foco, vamos desenvolver um pouco mais a lógica.
A ideia é ir capturando as teclas pressionadas, concatenar em uma string, converter a string para número e dividir pela quantidade de casas decimais.
Veja na tabela abaixo a lógica descrita acima, onde temos uma sequência de números digitados pelo usuário considerando 2 casas decimais:
Tecla pressionada na sequência | Valor String | Valor Decimal |
---|---|---|
1 |
1 |
0.01 |
0 |
10 |
0.10 |
2 |
102 |
1.02 |
Ou se tivermos 4 casas decimais:
Tecla pressionada na sequência | Valor String | Valor Decimal |
---|---|---|
1 |
1 |
0.0001 |
2 |
12 |
0.0012 |
3 |
123 |
0.0123 |
4 |
1234 |
0.1234 |
5 |
12345 |
1.2345 |
Ou se não tivermos casas decimais:
Tecla pressionada na sequência | Valor String | Valor Decimal |
---|---|---|
1 |
1 |
1 |
2 |
12 |
12 |
3 |
123 |
123 |
4 |
1234 |
1234 |
5 |
12345 |
2345 |
Trazendo a lógica acima para o código temos o seguinte:
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-input-porcentagem',
template: `
<div class="label">Label</div>
<div class="input">{{valor}}</div>
`,
styleUrls: ['./input-porcentagem.component.css']
})
export class InputPorcentagemComponent {
numeroCasasDecimais: number = 2;
valor: number = 0;
valorTexto: string = '';
@HostBinding('attr.tabindex') tabindex = 0;
@HostListener('keydown', ['$event.key'])
hostKeydown(key: string) {
this.valorTexto += key;
this.valor = +(this.valorTexto) /
(Math.pow(10, this.numeroCasasDecimais));
console.log(this.valorTexto, '->', this.valor);
}
}
Formatando o valor com PercentPipe
Para mostrar o valor do componente em formato de porcentagem vamos utilizar o PercentPipe
do próprio Angular.
Observação: para utilizar o valor | percent
no template, é necessário importar o módulo CommonModule
.
Com isto podemos efetuar o seguinte ajuste no template:
Também devemos lembrar que precisaremos dividir o valor do componente por 100 para obter o valor percentual, lembre-se de que 1
equivale 100%
, logo:
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-input-porcentagem',
template: `
<div class="label">Label</div>
<div class="input">{{valor | percent : digitsInfo}}</div>
`,
styleUrls: ['./input-porcentagem.component.css']
})
export class InputPorcentagemComponent {
numeroCasasDecimais: number = 2;
digitsInfo: string = `1.${this.numeroCasasDecimais}-${this.numeroCasasDecimais}`;
valor: number = 0;
valorTexto: string = '';
@HostBinding('attr.tabindex') tabindex = 0;
@HostListener('keydown', ['$event.key'])
hostKeydown(key: string) {
this.valorTexto += key;
this.valor = +(this.valorTexto) /
(100 * Math.pow(10, this.numeroCasasDecimais));
console.log(this.valorTexto, '->', this.valor);
}
}
Melhorando a experiência
Para melhorar a experiência do usuário precisamos tratar os seguintes casos:
- Bloquear caracteres que não sejam números
- Tratar o evento do backspace
- Tratar o caso onde o usuário fica digitando zero várias vezes
- Tratar o evento
onChanges
import { Component, HostBinding, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-input-porcentagem',
template: `
<div class="label">{{label}}</div>
<div class="input">{{valor | percent : digitsInfo}}</div>
`,
styleUrls: ['./input-porcentagem.component.css']
})
export class InputPorcentagemComponent implements OnChanges {
@Input() label: string = 'Label';
@Input() numeroCasasDecimais: number = 2;
@HostBinding('attr.tabindex') @Input() tabindex: number|string = 0;
digitsInfo: string = `1.${this.numeroCasasDecimais}-${this.numeroCasasDecimais}`;
valor: number = 0;
valorTexto: string = '';
ngOnChanges(changes: SimpleChanges): void {
if (changes.numeroCasasDecimais) {
this.digitsInfo =
`1.${this.numeroCasasDecimais}-${this.numeroCasasDecimais}`;
}
if (changes.tabindex) {
this.tabindex = +this.tabindex;
}
}
@HostListener('keydown', ['$event.key'])
hostKeydown(key: string) {
const teclasAceitas = /^(\d)|(Backspace)$/;
if (!teclasAceitas.test(key)) {
return;
}
if (key === 'Backspace') {
key = '';
this.valorTexto = this.valorTexto
.substr(0, this.valorTexto.length - 1);
} else if (!/\d/.test(key)) {
return;
}
this.valorTexto += key;
if (+this.valorTexto === 0) {
this.valorTexto = '';
}
this.valor = +(this.valorTexto) /
(100 * Math.pow(10, this.numeroCasasDecimais));
console.log(this.valorTexto, '->', this.valor);
}
}
Considerações
Apesar do artigo parecer ser grande e cheio de detalhes, com o tempo você irá se acostumar com todos os recursos apresentados neste artigo.
Essa mesma ideia de não se utilizar um <input>
para ter uma entrada de dados pode se aplicar por exemplo a um slider, organizador drag & drop, etc.
Podemos destacar como principais recursos abordados neste artigo: @HostListener
, @HostBinding
e PercentPipe
Em um próximo artigo vamos abordar a integração deste componente com o ReactiveForms
do Angular.
Exemplo do código completo:
Posts relacionados a este assunto: