Novidades do Angular 18

Conheça as principais novidades da versão 18 do framework Angular: zoneless, eventos no formGroup, fallback no ng-content, entre outras novidades.

Novidades do Angular 18
Novidades do framework Angular 18

Após aproximadamente 6 meses desde o lançamento da versão 17 do framework Angular, foi lançada recentemente a versão 18. Essa versão apresenta algumas novidades como:

  • O controle de fluxo passar a ser stable
  • Tratamento de erro (fallback) para o ng-content
  • Possibilidade de utilizar funções na propriedade redirect do Route
  • Eventos no FormGroup, FormControl e FormArray
  • Suporte para "zoneless change detection", ainda em modo experimental

Neste texto abordamos um pouco sobre essas novidades e também como atualizar o CLI para a versão 18.

Como atualizar o CLI do Angular

Para atualizar o CLI do Angular:

npm install -g @angular/cli@18

# Após a instalação é possível verificar
# a versão com o seguinte comando:
ng version

# Exemplo da saída do comando acima:
Angular CLI: 18.0.2
Node: 20.12.0
Package Manager: npm 10.8.1
OS: darwin arm64

Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1800.2 (cli-only)
@angular-devkit/core         18.0.2 (cli-only)
@angular-devkit/schematics   18.0.2 (cli-only)
@schematics/angular          18.0.2 (cli-only)

Controle de fluxo

Há pouco tempo foi apresentada uma nova sintaxe para controle de fluxo, permitindo escrever estruturas como if/else, loop for e switch de uma maneira diferente. A partir desta versão, 18, essa sintaxe passa a ser estável (stable) e não mais experimental.

Exemplo utilizando diretivas:

<button (click)="exibirDetalhes.set(!exibirDetalhes())">
  <ng-container *ngIf="exibirDetalhes()">
    Esconder detalhes
  </ng-container>
  <ng-container *ngIf="!exibirDetalhes()">
    Mostrar detalhes
  </ng-container>
</button>

<div *ngIf="exibirDetalhes()">
  <ng-container *ngFor="let item of itens()">
    <div>{{ item }}</div>
  </ng-container>
  <ng-container *ngIf="itens().length">
    <div>Sem itens</div>
  </ng-container>
</div>

Exemplo utilizando a nova sintaxe:

<button (click)="exibirDetalhes.set(!exibirDetalhes())">
  @if(exibirDetalhes()) {
    Esconder detalhes
  }
  @else {
    Mostrar detalhes
  }
</button>
@if(exibirDetalhes()) {
  <div>
    @for (item of itens(); track $index) {
      <div>{{ item }}</div>
    } @empty {
      <div>Sem itens</div>
    }
  </div>
}

Valor default para o ng-content

Antes da versão 18, era um pouco mais complicado atribuir um valor padrão (default) para o <ng-content>. Considere um componente bem simples, um CardComponent que tenha o seguinte template:

<div>
  <h1>{{titulo()}}</h1>
  <div>
    <ng-content></ng-content>
  </div>
</div>

Ao utilizar este componente, é possível projetarmos algo a mais devido à presença do <ng-content>, por exemplo:

<app-card titulo="Card com ng-content">
  <!-- Conteúdo projetado no ng-content -->
  <p>Lorem, ipsum dolor.</p>
  <em>Lorem ipsum dolor sit amet.</em>
</app-card>

Eventualmente, ao utilizar o componente você não irá projetar nada, e então podemos atribuir um valor padrão caso quem esteja utilizando o componente não projete nenhum conteúdo, por exemplo:

<app-card titulo="Card com ng-content">
</app-card>

Definindo um valor padrão para o ng-content

Antes da versão 18 do Angular:

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

@Component({
  selector: 'app-card-antigo',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './card-antigo.component.html'
})
export class CardAntigoComponent {
  titulo = input<string>('');
}

card-antigo.component.ts

<div>
  <h1>{{titulo()}}</h1>
  <div>
    <div #container>
      <ng-content></ng-content>
    </div>
    <div *ngIf="!container.children.length">
      NGContent não informado!
    </div>
  </div>
</div>

card-antigo.component.html

A partir da versão 18 ficou bem mais simples, veja a seguir:

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

@Component({
  selector: 'app-card',
  standalone: true,
  templateUrl: './card.component.html',
})
export class CardComponent {
  titulo = input<string>('');
}

card.component.ts

<div>
  <h1>{{titulo()}}</h1>
  <div>
    <ng-content>NGContent não informado!</ng-content>
  </div>
</div>

card.component.html

Para ambos os casos acima, caso nenhum conteúdo seja projetado, o Angular assumirá o valor default NGContent não informado!:

<app-card titulo="Card com ng-content">
  <div>Conteúdo dentro do card</div>
</app-card>

<app-card titulo="Card sem o ng-content">
</app-card>

Resultado do HTML gerado:

<app-card titulo="Card com ng-content">
  <div>
    <h1>Card com ng-content</h1>
    <div>
      <div>Conteúdo dentro do card</div>
    </div>
  </div>
</app-card>
<app-card titulo="Card sem o ng-content">
  <div>
    <h1>Card sem o ng-content</h1>
    <div>NGContent não informado!</div>
  </div>
</app-card>

Funções na propriedade redirect do Route

Outra novidade é a possibilidade de utilizarmos uma função na propriedade redirect na configuração das rotas. Isto dá mais liberdade para resolver alguns cenários.

Por exemplo, antes pesquisávamos um produto usando o endereço /produtos?id=123, mas agora o formato mudou para /produtos/123. Para manter a compatibilidade, podemos criar uma função na rota /produtos que fará o tratamento necessário e redirecionará para a nova rota. Veja o exemplo abaixo:

export const routes: Routes = [{
  path: 'produtos',
  redirectTo: (redirectData) => {
    const produtoId = redirectData.queryParams['id'];
    if (produtoId === undefined) {
      return 'not-found';
    }

    const router: Router = inject(Router);
    return router.createUrlTree(['produtos', produtoId]);
  },
}, {
  path: 'produtos/:id',
  component: ProdutoComponent
}, {
  path: 'not-found',
  component: NotFoundComponent
}, {
  path: '**',
  redirectTo: 'not-found'
}];

Eventos no FormGroup, FormControl e FormArray

As classes FormControl, FormGroup e FormArray agora possuem uma propriedade chamada events. Você pode se inscrever (subscribe) neste Observable para receber informações sobre diversos eventos. Os eventos disponíveis são:

  • FormResetEvent
    Emitido quando o formulário é resetado.
  • FormSubmittedEvent
    Emitido quando o formulário é submetido.
  • PristineChangeEvent
    Emitido quando o valor do formulário é modificado a partir do seu estado inicial.
  • StatusChangeEvent
    Emitido sempre que há uma mudança de status.
  • TouchedChangeEvent
    Emitido quando há alguma interação com o formulário, como clicar em um campo e depois fora dele.
  • ValueChangeEvent
    Emitido sempre que houver uma alteração no valor do formulário.

Exemplo:

// Emite o status do formulário quando houver uma
// alteração em relação ao último valor emitido
// Valores possíveis:
//   'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'
this.formGroup.events
  .pipe(
    filter(event => event instanceof StatusChangeEvent),
    map((event) => (event as StatusChangeEvent).status),
    distinctUntilChanged()
  )
  .subscribe((status) => {
    console.log('Novo status do formulário: ', status);
  });

Suporte para "zoneless change detection", ainda em modo experimental

Anteriormente, comentamos sobre o funcionamento do sistema de detecção de mudanças (change detection) do Angular: como funciona o change detection do Angular. Nesta versão (18), em modo experimental, o framework possibilita deixar de lado a lib Zone.js, por isso o "zoneless".

Dentre as vantagens desta nova feature, podemos citar:

  • Melhor interoperabilidade com outros frameworks e utilização de micro-frontends.
  • Renderização inicial e execução mais rápidas.
  • Bundle menor.
  • Stack trace mais legível.

Ao que parece, a equipe do Angular está focada em tornar o framework independente da lib Zone.js.

We’ve been working for several years towards a way of using Angular that doesn’t rely on zone.js, and we’re incredibly excited to share the first experimental APIs for zoneless!

fonte: https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe

Como utilizar o zoneless

Para utilizar este modo é bem simples. Primeiro modifique o app.config.ts adicionando o conteúdo abaixo:

// (imports)...

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    // ...
  ],
};

app.config.ts

Depois remova a referência zone.js no arquivo angular.json:

"polyfills": [
  // Remover a linha abaixo:
  "zone.js"
],

angular.json

Considerações

Para concluir, cobrimos apenas uma fração das novidades do Angular 18, mas neste link você pode ver a lista completa de novidades.

Links interessantes: