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
💡
Para quem já tem um projeto e precisa atualizar a versão do Angular, vale dar uma olhada neste link.

Testando o projeto recém-criado no navegador:

Testando o projeto recém-criado

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.

Entendendo o pure e standalone do Angular Pipe
Veja as diferenças de valores nos parâmetros pure e standalone de um Angular Pipe. Aprenda a usá-los para melhorar o desempenho e a reutilização.

Neste texto foi abordado sobre o que é o standalone e o que motivou sua criação

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:

Exemplo do código gerado pelo CLI

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.

{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

trecho do tsconfig.json

Exemplo de erro ao utilizar um componente não standalone com a configuração strictStandalone ativada

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.

Content-Security-Policy (CSP): como melhorar a segurança do seu site
A CSP (Content Security Policy) é uma camada na defesa para seu site. Neste texto exploramos uma visão detalhada do CSP, destacando sua importância e métodos para combater vulnerabilidades como XSS e clickjacking.
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
Exemplo de uso do autoCsp

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.

Exemplo de erro do CSP ao tentar incluir um novo bloco de script

inputs, outputs, e view queries passam a ser estáveis

Os recursos input, output, viewChild, viewChildren, contentChild e contentChildren passam a ser estáveis.

Angular
The web development framework for building modern apps.

Link da documentação oficial mostrando exemplos

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:

Exemplo do linkedSignal

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— signalcomputedinput, etc. However, applications often need to deal with data that is available asynchronously. A Resource 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:

Exemplo de uso do resource

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:

Tabela de compatibilidade entre versões do Angular, Node.js, TypeScript e Rxjs

Links interessantes: