Ao iniciar os estudos em Angular, aprendemos como criar Componentes utilizando classes, bem como propriedades e métodos. Assim como através do decorator @Component() informamos ao angular que determinada classe é um componente, para informarmos que determinada propriedade será um Input também fazemos uso de decorators, no caso @Input() para informar se determinada propriedade receberá dados de origem externa, como mostrado no exemplo abaixo:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<h1>{{ title }}</h1>`
})
export class ChildComponent {
@Input() title: string = '';
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child [title]="title" />`
})
export class ParentComponent {
title = "test title";
}
Neste artigo vamos falar um pouco sobre algumas melhorias introduzidas a partir da v16, e mostrar que embora havia uma outra forma de ser feito, as novidades ajudaram a melhorar demais a experiência nossa como desenvolvedor (DX), vamos conferir a seguir!
Input Options
Required
Uma pequena melhoria, porém há muito tempo requisitada pela comunidade, a partir da versão v16 podemos utilizar a nova opção required do input, nos ajudando a identificar erros caso ao utilizar o componente por ventura a gente se esqueça de passar a propriedade mandatória:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<h1>{{ title }}</h1>`
})
export class ChildComponent {
@Input({ required: true }) title: string = '';
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child />`
// Required input 'title' from component ChildComponent must be specified.ngtsc(-998008)
})
export class ParentComponent {}
Transform
Caso o valor esperado no input seja uma string, podemos passar o valor sem usar os colchetes "[]" (property binding), porém o mesmo não é verdade caso o tipo do input seja um number. Neste caso, se tentarmos passar uma string teremos este erro:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<button>{{ price }}</button>`
})
export class ChildComponent {
@Input() price: number = 0;
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child price="20" />`
// Type 'string' is not assignable to type 'number'.ngtsc(2322)
})
export class ParentComponent {}
Alternativa 1: Podemos utilizar [price]="20" e [withDiscount]="true", respeitando o tipo dos valores esperados no input do componente filho:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<button>{{ withDiscount ? price * 0.9 : price }}</button>`
})
export class ChildComponent {
@Input() price: number = 0;
@Input() withDiscount: boolean = false;
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child [price]="20" [withDiscount]="true" />`
})
export class ParentComponent {}
Alternativa 2: Para utilizarmos os inputs de forma mais elegante, ou seja, apenas price="20" e withDiscount (seria mais natural, afinal temos elementos html nativos que para passar uma propriedade boolean basta apenas escrever ou omitir a propriedade).
Porém, precisamos permitir que o componente filho possa "tranformar" o dado assim que chega, uma das formas de se fazer isso é através do lifecycle OnChanges ou utilizando getters e setters como no caso abaixo:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<button>{{ withDiscount ? price * 0.9 : price }}</button>`
})
export class ChildComponent {
#price: number = 0;
@Input() set price(value: string | number) {
typeof value === 'number'
? (this.#price = value)
: (this.#price = isNaN(Number(value)) ? 0 : Number(value));
}
get price(): number {
return this.#price;
}
#withDiscount: boolean = false;
@Input() set withDiscount(value: string | boolean) {
typeof value === 'boolean'
? (this.#withDiscount = value)
: (this.#withDiscount = true);
}
get withDiscount(): boolean {
return this.#withDiscount;
}
// dá para ser feito mas precisa escrever demais!
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child price="20" withDiscount />`
// também funciona [price]="20" ou [price]="'20'" ou
// [withDiscount]="false" ou [withDiscount]="true" ou withDiscount
})
export class ParentComponent {}
Felizmente, a partir da versão v16.1, podemos utilizar a nova opção transform do input, passando uma função para aplicar a sua transformação, temos 2 funções prontas para os casos acima, numberAttibute e booleanAttribute, ou podemos criar nossa própria função customizada!
import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<label>{{ label }}</label>
<button>{{ withDiscount ? price * 0.9 : price }}</button>
`
})
export class ChildComponent {
@Input({ transform: numberAttribute }) price: number = 0;
@Input({ transform: booleanAttribute }) withDiscount: boolean = false;
@Input({ transform: (value: string) => value.toLocaleUpperCase() }) label = '';
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child price="20" withDiscount label="total" />`
// será exibido "TOTAL" invés de "total" no componente filho
})
export class ParentComponent {}
Input Signal
Por mais que a v17 esteja recheada de novidades excelentes como Built-in Control Flow, Deferrable Views dentre outras tantas, ainda não temos a nova API para signal components.
Este passo com certeza está incluso no roadmap recente do time do angular, e tem como objetivo também contribuir para tornar o Zone.JS opcional (zoneless applications).
Alternativa 1: Hoje mesmo podendo utilizar signals para lidar com o estado dentro do componente, ainda com o input tradicional de antes, precisaríamos passar o valor do tipo signal para o input:
import { Component, Input, signal } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<h1>{{ title() }}</h1>
`
})
export class ChildComponent {
@Input() title!: Signal<string>;
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child title="title" />`
// não queremos passar apenas o valor do signal com title(), e sim o signal todo
})
export class ParentComponent {
title = signal('Input Signals');
}
Alternativa 2: Utilizar a mesma técnica de getter and setter demonstrada acima e criar um signal privado para isso:
import { Component, Input, signal, computed } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<h1>{{ title }}</h1>
<h1>{{ titleValue() }}</h1>
`
})
export class ChildComponent {
#title = signal<string>('');
@Input() set title(value: string) {
this.#title.set(value);
}
get title(): string {
return this.#title();
};
// podemos usar computed no lugar do getter também
titleValue = computed(() => this.#title());
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child title="Input Signals" />`
})
export class ParentComponent {}
Para quem já está utilizando no mínimo a v17.1 já temos como experimentar a nova forma de declarar inputs, e o mais importante, ela já considera o valor da propriedade dentro do componente desde o início como um signal. A nova api de input, bem como outras que virão para serem utilizadas dentro do componente, a exemplo da função inject(), serão todas funcionais. Confira o exemplo final:
import { Component, InputSignal, input } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `
<h1>{{ title() }}</h1>
`
})
export class ChildComponent {
title = input<string>()
// exemplos de uso com valor default, required e transform
title2 = input('')
title3 = input.required<string>()
title4 = input('', { transform: (val: string) => val.trim() })
}
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `<app-child title="Input Signals" />`
})
export class ParentComponent {}
Espero que tenham gostado das informações, compartilhe com seus amigos que gostam e estão aprendendo mais sobre angular, e até o próximo! Grande abraço!