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.
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:
- 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
. - A versão específica do TypeScript para o projeto em questão, encontrada no arquivo
package.json
.
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.
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:
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.
Na tabela abaixo há uma lista de versões do Angular vs Node.js vs TypeScript vs RxJS. Retirei essa tabela desse link.
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
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 oEventEmitter
, 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: UseprovideRouter
orRouterModule
/RouterModule.forRoot
instead. This module was previously used to provide a helpful collection of test fakes, most notably those forLocation
andLocationStrategy
. These are generally not required anymore, asMockPlatformLocation
is provided inTestBed
by default. However, you can use them directly withprovideLocationMocks
.
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: