Novidades do Angular 19
Lançada no dia 19 de novembro de 2024, a versão 19 do framework Angular traz novidades interessantes.
Atualizando o CLI
Antes de falar sobre as novidades do Angular 19, o script abaixo mostra como instalar a nova versão do CLI e também cria um novo projeto para explorarmos as novidades:
# Instalando o CLI da nova versão:
npm i @angular/cli@19 -g
# Conferindo a versão:
ng version
Angular CLI: 19.0.0
Node: 22.11.0
Package Manager: npm 10.9.0
OS: darwin arm64
# Criando um novo projeto:
ng new versao19 --skip-git --skip-tests
# Executando o novo projeto:
cd versao19/
npm start
Testando o projeto recém-criado no navegador:
Standalone agora é default
A partir dessa versão (Angular 19), não é mais necessário declarar explicitamente standalone: true
para componentes, pipes e diretivas. Essa configuração, padrão a partir dessa versão e introduzida no Angular 14, permite que esses elementos sejam utilizados de forma independente, sem a necessidade de serem declarados em um módulo.
Observe no código abaixo gerado pelo CLI, que não é necessário declarar explicitamente o standalone: true
. Quando omitido, o padrão já é true
:
Strict standalone
Ainda falando sobre o standalone, uma forma de garantir que todos os componentes, pipes e diretivas do seu projeto sejam standalone, é utilizando a opção strictStandalone: true
no arquivo tsconfig.json. Ao configurar essa opção, você está informando ao compilador que todos os componentes, pipes e diretivas do seu projeto devem ser declarados como standalone. Isso implica que, caso o compilador identifique algum componente, pipe ou diretiva que não seja standalone, um erro será gerado durante o processo de build.
autoCsp - hash-based - Content Security Policy
Essa nova funcionalidade, ainda em developer preview, adiciona um hash no arquivo index.html
gerado durante o processo de build. O objetivo é simples: aumentar a segurança da aplicação nos navegadores. Esse recurso, conhecido como Content Security Policy (CSP), oferece um maior controle sobre os recursos que o navegador pode carregar. Diferentemente da abordagem tradicional, que lista apenas as origens permitidas, a CSP hash-based exige que os recursos sejam identificados por meio de um hash criptográfico único, garantindo uma camada adicional de proteção.
Using hash-based CSP, the browser will add the hash of every inline script to the CSP. Each script will have a unique hash associated with it. That will prevent an attacker from running a malicious script on your page because for the browser to execute the script, its hash needs to be present in the CSP.
https://blog.angular.dev/meet-angular-v19-7b29dfd05b84
A imagem abaixo mostra mostra um trecho do arquivo index.html que foi gerado durante o processo de build e recebeu um hash. Esse hash é como uma impressão digital única do conteúdo dos scripts incluídos nesse arquivo. Ao adicionar ou modificar um bloco de script, o hash deixa de ser válido, pois ele representa uma nova "impressão digital" do arquivo. Consequentemente, o navegador, ao verificar a diretiva Content Security Policy (CSP), não encontra uma correspondência entre o hash especificado na diretiva e o hash calculado localmente para o script. Por essa razão, o script não é executado.
inputs, outputs, e view queries passam a ser estáveis
Os recursos input
, output
, viewChild
, viewChildren
, contentChild
e contentChildren
passam a ser estáveis.
linkedSignal
Em modo experimental, o linkedSignal
é essencialmente um signal que reage às mudanças de outro signal. No exemplo abaixo, é possível observar que, ao alterar o valor de opcoes
, o valor de opcaoSelecionada
é automaticamente atualizado.
import { CommonModule } from '@angular/common';
import {
Component,
linkedSignal,
signal
} from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, CommonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
opcoes = signal<Array<{ id: number; label: string }>>([
{ id: 1, label: 'Item 1' },
{ id: 2, label: 'Item 2' },
{ id: 3, label: 'Item 3' },
]);
opcaoSelecionada = linkedSignal(() => this.opcoes()[0]);
selecionarItem(id: number) {
const itemSelecionado = this.opcoes()
.find((a) => a.id === id);
if (itemSelecionado === undefined) {
return;
}
this.opcaoSelecionada.set(itemSelecionado);
}
gerarOutrasOpcoes() {
this.opcoes.set([
{ id: 4, label: 'Item 4' },
{ id: 5, label: 'Item 5' },
{ id: 6, label: 'Item 6' },
]);
}
}
<div>
Opções:
<pre>{{ opcoes() | json }}</pre>
</div>
<div>
Selecionada:
<pre>{{ opcaoSelecionada() | json }}</pre>
</div>
<hr>
<div>
@for (opcao of opcoes(); track opcao.id) {
<button (click)="selecionarItem(opcao.id)">
Selecionar: {{ opcao.label }}
</button>
}
</div>
<hr>
<div>
<button (click)="gerarOutrasOpcoes()">
Gerar outras opções
</button>
</div>
Resultado:
resource
Também em modo experimental, o resource()
tem o objetivo de trabalhar de forma similar ao signal
, porém para operações assíncronas, como por exemplo, requisições HTTP.
Most signal APIs are synchronous—signal
,computed
,input
, etc. However, applications often need to deal with data that is available asynchronously. AResource
gives you a way to incorporate async data into your application's signal-based code.
https://angular.dev/guide/signals/resource
Abaixo há um exemplo de uso onde inicialmente uma lista de itens é carregada e renderizada em uma tabela. Então, ao clicar no botão "detalhes" na tabela, preenchemos o valor do signal idItemSelecionado
e automaticamente o código dentro do resource itemSelecionadoResource
efetua uma requisição à API. O resultado é computado em itemSelecionado
, que por sua vez, é apresentado na tela.
import { CommonModule } from '@angular/common';
import {
Component,
computed,
resource,
signal
} from '@angular/core';
@Component({
selector: 'app-root',
imports: [CommonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
// Carrega uma lista de itens
itensResource = resource({
loader: async () => {
const url = new URL('/', 'http://localhost:3000/');
const fetchResponse = await fetch(url);
return await fetchResponse.json();
},
});
itens = computed(() => this.itensResource.value());
// Trata os detalhes de um item selecionado
idItemSelecionado = signal<number | null>(null);
itemSelecionadoResource = resource({
request: () => ({ id: this.idItemSelecionado() }),
loader: async ({ request }) => {
if (request.id === null) {
return null;
}
const baseUrl = 'http://localhost:3000/';
const path = request.id !== null ? `/${request.id}` : '/';
const url = new URL(path, baseUrl);
const fetchResponse = await fetch(url);
return await fetchResponse.json();
},
});
itemSelecionado = computed(() =>
this.itemSelecionadoResource.value()
);
selecionarItem(id: number) {
this.idItemSelecionado.set(id);
}
}
<table>
<thead>
<tr>
<td>Id</td>
<td>Label</td>
<td></td>
</tr>
</thead>
<tbody>
@for (item of itens(); track item.id) {
<tr>
<td>{{ item.id }}</td>
<td>{{ item.label }}</td>
<td>
<button
(click)="selecionarItem(item.id)">
Detalhes
</button>
</td>
</tr>
@if (item.id === idItemSelecionado()) {
<tr>
<td colspan="3">
<pre>
@if (itemSelecionadoResource.isLoading()) {
Carregando...
} @else {
{{ itemSelecionado() | json }}
}
</pre>
</td>
</tr>
}
}
</tbody>
</table>
Resultado:
Considerações
Aqui, destacamos apenas algumas das novidades, neste link você encontra a lista completa da versão 19. Além disso, abaixo apresentamos uma tabela de compatibilidade entre as versões do Angular, Node.js, TypeScript e RxJS:
Links interessantes: