Angular 17.3: conheça a nova API output() e Host Attribute Token

Confira as principais atualizações do Framework Frontend Angular 17.3. Esta versão apresenta uma nova API de output, o HostAttributeToken, suporte ao TypeScript 5.4, entre outras melhorias.

Uma mesa com um Notebook em cima. Na tela do notebook tem o logotipo do Framework Angular
Angular 17.3: conheça a nova API output() e Host Attribute Token

Desde o lançamento da versão 17.0.0 do framework frontend Angular, no dia 8 de novembro de 2023, várias novidades foram introduzidas. Inclusive temos um post dedicado ao lançamento desta versão.

Pouco tempo depois, no dia 13 de março de 2024, foi lançada a versão 17.3.0 do Angular. Atualmente, em abril de 2024, já estamos na versão 17.3.3.

Neste texto vou destacar algumas novidades que surgiram nesta minor, 17.3.

💡
Caso você não esteja familiarizado com os termos, major, minor e patch, eles são termos utilizados para dar valor semântico nos códigos de versão. Por exemplo, 17.3.1 significa:

17: Major - Indica mudanças incompatíveis com versões anteriores.
3: Minor - Indica adição de novas funcionalidades, sem quebrar a compatibilidade.
1: Patch - Indica correções de bugs, sem quebrar a compatibilidade.

Link com mais detalhes sobre semantic version.

Novidades do Angular 17.3: Suporte ao TypeScript 5.4

Nesta versão do Angular, 17.3, foi adicionado o suporte para TypeScript 5.4. Não vou entrar nos detalhes das novidades do TypeScript, mas deixo este link com a lista completa. Entretanto, vou abordar rapidamente um assunto que poderá ajudá-lo e está relacionado a versão do Angular, TypeScript e VS Code.

Definindo a versão do TypeScript no VS Code para trabalhar com versões anteriores do Angular

Em linhas gerais, no VS Code, podemos escolher entre duas versões do TypeScript:

  1. A versão suportada pelo próprio VS Code, que se limita ao suporte da linguagem, ou seja, você terá o syntax highlight e validação de erros na tela, porém, para realizar a transpilação do código, ainda é necessário instalar o compilador tsc.
  2. A versão específica do TypeScript para o projeto em questão, encontrada no arquivo package.json.
🔗
Visual Studio Code includes TypeScript language support but does not include the TypeScript compiler, tsc.
Link

Por termos 2 versões, eventualmente elas podem não ser a mesma. Isso significa que você pode estar trabalhando em um projeto com TypeScript 5.4.2, enquanto a configuração do seu VS Code permanece na versão 5.3.2. Como resultado, você pode inadvertidamente escrever um código válido na versão 5.4.2, mas inválido na 5.3.2. Isso pode levar o VS Code a destacar erros no código, mesmo que o projeto compile com sucesso. Isso ocorre porque a versão do compilador TypeScript é definida no arquivo package.json do seu projeto, e não pela configuração global do VS Code.

No exemplo a seguir, temos um projeto na versão 17.3.0 do Angular, com suporte ao TypeScript 5.4. O VS Code está na versão 1.87.2, que oferece suporte nativo para o TypeScript 5.3.2. No entanto, mesmo com o Angular utilizando o TypeScript 5.4, ao adicionar um bloco de código válido nesta versão, o VS Code sinaliza um erro. Apesar disso, o projeto ainda é compilável com sucesso.

Printscreen do VS code mostrando um erro de TypeScript no meio do código fonte. Abaixo o Terminal mostra que o projeto está compilando corretamente.
VS Code utilizando a versão 5.3.2 mas a versão instalada no projeto é a 5.4.3

Para resolver esse problema, é necessário informar ao VS Code que desejamos trabalhar com a versão 5.4 do TypeScript, que é a mesma versão que o projeto Angular utiliza. Para fazer isso, basta abrir qualquer arquivo .ts e pressionar:

  • cmd + shift + p (MacOS)
  • ctrl + shift + p (Windows)

Depois digite "typescript: select typescript versions". Aparecerá uma lista de opções, como a imagem a seguir ilustra:

Lista de versões do TypeScript no VS Code. Neste exemplo temos as versões 5.4.3 e 5.3.2
Selecionando a versão do TypeScript no VS Code

Basta selecionar a opção desejada. Neste exemplo, a opção desejada é a do projeto Angular, que é a 5.4.

No mundo real, você precisará fazer esta seleção quando estiver trabalhando com o VS Code atualizado e um projeto TypeScript mais antigo, por exemplo, um projeto em Angular 8 nos dias atuais.

Printscreen do VS code mostrando código válido de TypeScript. Abaixo o Terminal mostra que o projeto está compilando corretamente.
Após selecionar a versão 5.4.3 no VS Code o erro desaparece

Na tabela abaixo há uma lista de versões do Angular vs Node.js vs TypeScript vs RxJS. Retirei essa tabela desse link.

Tabela de versões do Angular vs Node.js vs TypeScript vs RxJS
Tabela de versões do Angular vs Node.js vs TypeScript vs RxJS

Nova API output()

Na versão 16 do Angular, foi introduzido o Signals. Inclusive comentamos por aqui: Introdução ao Angular Signals.

Desde então, deu-se início a construção de uma nova API que gira em torno do Signals. Por exemplo, quando queremos que um componente possa receber uma informação externa, utilizamos o @Input() ou o input, lembrando que o último está integrado ao Signals.

Nesta versão, 17.3, liberaram a API output como developer preview, que no seu uso é similar ao @Output. Este recurso deve ser utilizado para externalizar alguma informação do componente. Por exemplo:

import { Component, input, output } from '@angular/core';

@Component({
 selector: 'app-output-test',
 standalone: true,
 template: `<button (click)="test()">
  Button: {{ buttonLabel() }}
 </button>`,
})
export class OutputTestComponent {
 onClick = output<Date>();
 buttonLabel = input<string>('Test');

 test() {
  this.onClick.emit(new Date());
 }
}

output-test.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OutputTestComponent } from './output-test.component';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [OutputTestComponent, CommonModule],
 templateUrl: './app.component.html',
 styleUrl: './app.component.css',
})
export class AppComponent {
 curDate1: Date = new Date();
}

app.component.ts

<h1>Output</h1>
<app-output-test
 buttonLabel="Test novo output"
 (onClick)="curDate1 = $event"></app-output-test>

 <div>curDate1: {{ curDate1 | date : 'longTime' }}</div>

app.component.html

Animação mostrando que ao clicar no botão, o texto abaixo é atualizado com o valor da data corrente
Exemplo do output()

Outras vantagens que podemos destacar no uso do output():

  • está em linha com o Signals, assim como os inputs, queries e model();
  • a API foi simplificada, ficando somente o que é relevante para a funcionalidade. Comparando com o antigo @Output() onde utilizávamos o EventEmitter, ao efetuar um .subscribe() terá 3 possibilidades de callback: success, error e complete, já no output não, é só um callback:
onClick = output<Date>();

this.onClick.subscribe((value: Date) =>
 console.log(value)
);

// ***

@Output() onClick2 = new EventEmitter<Date>();

this.onClick2.subscribe({
 next: (value: Date) => console.log(value),
 error: (error: any) => console.error(error),
 complete: () => console.log('complete'),
});

Link da documentação oficial a respeito do novo output.

HostAttributeToken

Com o HostAttributeToken, podemos passar um valor estático ao construir um componente, mas antes que você afirme: "ok, eu já tenho o @Input ou input para passar valores para o componente, o que o HostAttributeToken faz de diferente?"

A distinção principal reside no fato de que os valores transmitidos via HostAttributeToken não acionam a detecção de mudanças (change detection), enquanto os valores passados através de @Input()/input(), mesmo que estáticos, ativam a detecção de mudanças. Portanto, se o valor que você deseja passar para o componente não mudará ao longo do ciclo de vida deste, o HostAttributeToken é uma escolha apropriada.

import {
 Attribute,
 Component,
 HostAttributeToken,
 inject,
} from '@angular/core';

@Component({
 selector: 'app-host-attr-test',
 standalone: true,
 template: `
  <input [type]="inputType" [value]="inputValue" />
 `,
})
export class HostAttrTestComponent {
 inputType: string = '';

 inputValue: string | null = inject(
  new HostAttributeToken('value'),
  { optional: true }
 );

 constructor(@Attribute('type') type: string) {
  this.inputType = type;
 }
}

host-attr-test.component.ts

Quem for utilizar o componente do exemplo acima, poderá passar um valor direto no atributo do seletor:

<app-host-attr-test
 type="search"
 value="teste"></app-host-attr-test>

RouterTestingModule deprecation

O RouterTestingModule foi marcado como deprecated.

Deprecated: Use provideRouter or RouterModule/RouterModule.forRoot instead. This module was previously used to provide a helpful collection of test fakes, most notably those for Location and LocationStrategy. These are generally not required anymore, as MockPlatformLocation is provided in TestBed by default. However, you can use them directly with provideLocationMocks.

https://angular.io/api/router/testing/RouterTestingModule

No lugar devemos usar o RouterModule ou provideRouter. Veja no exemplo abaixo:

import { Routes } from '@angular/router';
import { Page1Component } from './page1/page1.component';

export const routes: Routes = [
 { path: 'page1', component: Page1Component },
 { path: 'page2', component: Page1Component },
 { path: '**', redirectTo: '/page1' },
];

app.routes.ts

import { Location } from '@angular/common';
import {
 TestBed,
 fakeAsync,
 tick,
} from '@angular/core/testing';
import { Router, RouterModule } from '@angular/router';

import { routes } from './app.routes';
import { Page1Component } from './page1/page1.component';
import { Page2Component } from './page2/page2.component';

describe('RouterModule', () => {
 let location: Location;
 let router: Router;

 beforeEach(() => {
  TestBed.configureTestingModule({
   imports: [
    Page1Component,
    Page2Component,
    RouterModule.forRoot(routes),
   ],
  });

  router = TestBed.inject(Router);
  location = TestBed.inject(Location);

  router.initialNavigation();
 });

 it('navigate to "" redirects you to /page1', fakeAsync(() => {
  router.navigate(['']);
  tick();
  expect(location.path()).toBe('/page1');
 }));

 it('navigate to "page2" takes you to /page2', fakeAsync(() => {
  router.navigate(['/page2']);
  tick();
  expect(location.path()).toBe('/page2');
 }));
});

routes.spec.ts

Considerações

O framework Angular está evoluindo constantemente. Tentei citar as novidades mais relevantes desta minor, 17.3. Abaixo há algumas referências interessantes: