Introdução ao Angular 17

Em novembro de 2023, foi lançada a aguardada versão 17 do Angular. Neste texto exploraremos as inovações e melhorias introduzidas nesta nova versão.

Banner de divulgação - Introdução ao Angular 17
Introdução ao Angular 17

A versão mais recente do Angular é a 17, lançada em novembro de 2023, e destaca-se como a versão ativa. Enquanto isso, a versão 16 foi designada como Long-Term Support (LTS), conforme ilustrado na tabela abaixo:

Version Status Released Active ends LTS ends
^17.0.0 Active 2023-11-08 2024-05-08 2025-05-15
^16.0.0 LTS 2023-05-03 2023-11-08 2024-11-08
^15.0.0 LTS 2022-11-18 2023-05-03 2024-05-18

Dados obtidos na documentação oficial em jan/2024.

A versão 17 traz uma série de novidades e melhorias que tornam o framework ainda mais poderoso e fácil de usar. Neste post vamos comentar sobre algumas dessas mudanças.

Atualizando o CLI

Primeiramente atualizei o CLI do Angular para a versão 17 utilizando o npm update e na sequência conferi a versão com o ng version:

$ npm update -g @angular/cli
$ ng version

Angular CLI: 17.0.9
Node: 18.18.2

Com o CLI atualizado, criei um novo projeto com o comando abaixo:

$ ng new --minimal --defaults --skip-git --strict angular17

Os parâmetros utilizados no comando acima tem o seguinte objetivo:

  • --minimal cria o projeto sem um framework de testes
  • --defaults desabilita as perguntas que aparecem no prompt, por exemplo, qual opção para estilos você quer utilizar? CSS, SCSS, etc
  • --skip-git não cria um repositório git
  • --strict habilita o modo strict que torna as regras de codificação mais rigorosas e ajuda a detectar erros comuns de forma mais proativa durante o desenvolvimento. Se quiser saber mais: https://angular.io/guide/strict-mode
💡
Para listar os parâmetros do comando new basta executar o comando ng new help

Executei o npm start para testar o novo projeto:

Navegador no endereço http://localhost:4200 exibindo o novo projeto Angular 17 criado
Novo projeto em execução

Nova sintaxe para templates

Para melhorar a experiência do desenvolvedor, o Angular lançou uma nova sintaxe para o controle de fluxo. Agora, podemos escrever as diretivas ngFor, ngIf e ngSwitch com uma sintaxe diferente. Além de achar essa sintaxe particularmente mais intuitiva, ela também proporciona melhorias no desempenho e no tempo de compilação.

Abaixo um exemplo de código que utiliza um pouco das diretivas mencionadas anteriormente no formato "tradicional":

<div *ngIf="mostrarMensagem; else loading">Olá</div>
<ng-template #loading>
  <div class="loading">Carregando...</div>
</ng-template>

<div *ngFor="let item of itens">{{ item.nome }}</div>
<div *ngIf="itens.length === 0">Sem itens</div>

<ng-container [ngSwitch]="perfil">
  <div *ngSwitchCase="'admin'">Admin</div>
  <div *ngSwitchCase="'user'">User</div>
  <div *ngSwitchDefault>-</div>
</ng-container>

Agora podemos reescrever utilizando a nova sintaxe. Lembrando que o uso desta nova sintaxe não é obrigatório.

@if (mostrarMensagem) {
  <div>olá</div>
} @else {
  <div class="loading">Carregando...</div>
}

@for (item of itens; track $index) {
  <div>{{ item.nome }}</div>
} @empty {
  <div>Sem itens</div>
}

@switch (perfil) {
  @case ('admin') {
    <div>Admin</div>
  }

  @case ('user') {
    <div>User</div>
  }

  @default {
    <div>-</div>
  }
}

Dando uma opinião totalmente pessoal, esta nova sintaxe me lembrou o Razor do ASP.NET. Particularmente, achei que o código fica mais legível, mas é apenas uma opinião pessoal.

Input value transform

Um comportamento bem comum e que foi otimizado é o uso de um valor boolean em um @Input(). Para entender o problema veja o componente abaixo e seu @Input:

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-loading",
  standalone: true,
  template: `
    @if (show) {
    <div>loading...</div>
    }
  `,
})
export class LoadingComponent {
  @Input() show: boolean = false;
}

Se utilizarmos este componente da seguinte forma...

<app-loading show></app-loading>

...vamos pegar o seguinte erro:

NG2: Type 'string' is not assignable to type 'boolean'.

Para corrigir este comportamento precisamos reescrever o código para:

<app-loading [show]="true"></app-loading>

Para evitar a necessidade de declarar explicitamente o [show]="true", nesta versão podemos utilizar o transform: booleanAttribute:

import { Component, Input, booleanAttribute } from "@angular/core";

@Component({
  selector: "app-loading",
  standalone: true,
  template: `
    @if (show) {
    <div>loading...</div>
    }
  `,
})
export class LoadingComponent {
  @Input({ transform: booleanAttribute }) show: boolean = false;
}

Então podemos escrever da seguinte forma:

<app-loading show></app-loading>

Deferrable views

Este novo recurso representa uma grande evolução para o lazy loading. Para aqueles que não estão familiarizados, o lazy loading no Angular é uma técnica que adia o carregamento de recursos específicos, como módulos ou componentes, até o momento em que são realmente necessários durante a execução do aplicativo. Em vez de carregar todos os recursos de uma vez no início, o aplicativo carrega esses recursos dinamicamente conforme são requisitados.

Com essa evolução, tornou-se muito simples isolar partes do template e carregá-las de uma forma que seja interessante para a aplicação. Por exemplo, você pode carregar um componente mais custoso apenas se ele estiver na área visível do navegador do usuário ou ativá-lo após algum gatilho, como após alguns segundos ou alguma interação do usuário.

Deferrable views, also known as @defer blocks, are a powerful tool that can be used to reduce the initial bundle size of your application or defer heavy components that may not ever be loaded until a later time. 

https://angular.io/guide/defer

Para controlarmos isto, podemos utilizar o @defer () { }. O que estiver dentro do bloco @defer não será carregado logo de início, ou seja, ele será carregado de forma "preguiçosa" (lazy loading). Seu conteúdo só será de fato carregado quando ocorrer algum gatilho (trigger), alguma condição for satisfeita ou o navegador ficar ocioso (idle).

The content of the main @defer block is the section of content that is lazily loaded. It will not be rendered initially, and all of the content will appear once the specified trigger or when condition is met and the dependencies have been fetched. By default, a @defer block is triggered when the browser state becomes idle
@defer {
  <large-component />
} @placeholder (minimum 500ms) {
  <p>Placeholder content</p>
}

Outro recurso interessante, embora não seja obrigatório, é o @placeholder. O conteúdo dentro do @placeholder é exibido enquanto o conteúdo do @defer não está pronto para ser apresentado na tela. Para facilitar o entendimento deixei um exemplo abaixo. Neste caso, o componente é carregado após 2 segundos ou quando o usuário realiza alguma interação, como um clique. Enquanto nenhuma dessas condições for satisfeita, o Angular exibirá o conteúdo que está dentro do @placeholder. Posteriormente, quando o componente estiver pronto para uso, o Angular substituirá o conteúdo do @placeholder pelo conteúdo real do componente:

import { Component, signal } from "@angular/core";

@Component({
  selector: "app-lista",
  standalone: true,
  template: `
    @for (item of itens(); track $index) {
      <div>{{ item }}</div>
    }
  `,
  styles: ``,
})
export class ListaComponent {
  itens = signal<string[]>(["Item 1", "Item 2"]);
}

lista.component.ts

import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { ListaComponent } from "./lista/lista.component";

@Component({
  selector: "app-root",
  standalone: true,
  imports: [CommonModule, RouterOutlet, ListaComponent],
  template: `
    <h1>Welcome to {{ title }}!</h1>
    @defer (on interaction; on timer(2s)) {
      <app-lista></app-lista>
    } @placeholder {
      <span>Carregando o componente lista...</span>
    }

    <router-outlet></router-outlet>
  `,
  styles: [],
})
export class AppComponent {
  title = "angular17";
}

app.component.ts

Como resultado, observe a seguir que, após 2 segundos ou ao clicar na área do componente, o app-loading é renderizado, mostrando uma lista com 2 itens.

Navegador mostrando o projeto em execução no localhost:4200. Após alguns segundos ou interagir com o ListaComponent, o mesmo é carregado sob demanda
ListaComponent sendo carregando conforme demanda

Ao compilar o projeto é gerado um arquivo exclusivo para o componente ListaComponent:

Initial Chunk Files   | Names           |  Raw Size | Estimated Transfer Size
chunk-R4WX5LRS.js     | -               | 102.95 kB |                31.02 kB
main-3DIZALSD.js      | main            |  78.22 kB |                19.75 kB
polyfills-LZBJRJJE.js | polyfills       |  32.69 kB |                10.59 kB
styles-5INURTSO.css   | styles          |   0 bytes |                 0 bytes

                      | Initial Total   | 213.86 kB |                61.36 kB

Lazy Chunk Files      | Names           |  Raw Size | Estimated Transfer Size
chunk-7K2PXFPN.js     | lista-component | 531 bytes |               531 bytes

Este arquivo (chunk-7K2PXFPN.js) é carregado automaticamente quando uma das condições do bloco @defer for satisfeita.

Falando das condições (triggers) do @defer, na documentação oficial podemos encontrar várias opções, dentre elas podemos destacar o carregando do conteúdo quando:

  • @defer (on viewport) { } - o componente entra na área visível do navegador do usuário
  • @defer (on hover) { } - o usuário passa o mouse em cima
  • @defer (on timer(500ms)) { } - se passam 500ms após o carregando da aplicação
  • @defer (when cond) { } - alguma condição for verdadeira, por exemplo, pode ser uma variável: @defer (when estaCarregado) { ... }

Link: https://angular.io/guide/defer

Considerações

Estas foram algumas das novidades no Angular 17. Além disso, destacam-se aprimoramentos no Server-Side Rendering (SSR), a adoção do Vite e esbuild, a introdução de novos lifecycle hooks, entre outros recursos. Abaixo, você encontrará links úteis falando um pouco mais sobre a versão 17: