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!

Sugestões de assuntos? Envie aqui
Copyright © 2024 Angularizando