Поток управления декларативным циклом в Angular 17

Недавние релизы Angular привнесли множество дополнений и улучшений. Судя по всему, в будущем ожидаются и другие приятные сюрпризы. Angular Renaissance или Momentum, следующая версия (v17), порадует нас дополнительными качественными нововведениями, одним из которых является новый встроенный синтаксис шаблонов потока управления.

Как следует из названия, это улучшение представляет собой новый декларативный синтаксис записи потока управления в шаблоне, обеспечивая тем самым функциональность *ngIf, *ngFor и *ngSwitch (поток управления на основе директив) в самом фреймворке.

Вопрос синтаксиса шаблонов обсуждался некоторое время в команде и сообществе Angular, которые предлагали собственные решения. После тщательных дискуссий для поддержки потока управления шаблонов был выбран вариант сообщества  —  @-syntax, что послужило подтверждением доверия членам комьюнити со стороны команды.

Больше всего меня заинтересовал поток управления циклом @-for из-за нового поддерживающего блока @-empty, который показывает шаблон, когда в списке нет элементов:

@for (product of products; track product.title) {
<tr>
<td>{{ product.title }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.category }}</td>
</tr>
} @empty {
<p>No products added yet!</p>
}

Новый синтаксис шаблонов позволяет удалить все элементы ng-container и ng-template в шаблоне, которые поддерживали *ngIf и *ngFor. Это делает шаблоны более компактными и обеспечивает лучший пользовательский опыт.

В статье я покажу, как теперь выглядит код цикла for в шаблоне после использования встроенного синтаксиса потока управления, а также расскажу о своем заблуждении относительно блока @-empty при итерации по данным, загружаемым асинхронно (наблюдаемые результаты или значения сигналов только для чтения из функции toSignal). Итак, перейдем к примеру!


В данном примере будет использоваться компонент Products, который выводит список товаров в виде таблицы. Ниже показана итерация, выполняемая с помощью текущей структурной директивы *ngFor:

import { Component, inject } from '@angular/core';
import { AsyncPipe, CommonModule } from '@angular/common';
import { ProductService } from '../product.service';

@Component({
selector: 'app-products',
standalone: true,
imports: [CommonModule, AsyncPipe],
template: `
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Price</th>
<th>Brand</th>
<th>Category</th>
</tr>
</thead>

<tbody>
<ng-container *ngIf="products$ | async as products">
<ng-container *ngIf="products.length; else noResults">
<tr *ngFor="let product of products">
<td>{{ product.title }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.category }}</td>
</tr>
</ng-container>

<ng-template #noResults>
<p>No results yet!</p>
</ng-template>
</ng-container>
</tbody>
</table>
`,
styleUrls: ['./products.component.scss'],
})
export class ProductsComponent {
products$ = inject(ProductService).getProducts();
}

Здесь используются элементы ng-template | ng-container. Сначала мы проводим проверку на null, предотвращая начальное значение null, выдаваемое из асинхронного конвейера, убеждаемся, что при выдаче получаем результат из наблюдаемой переменной products, а затем проверяем, пуст ли список, чтобы вывести сообщение по умолчанию или отобразить продукты в таблице.

Как бы все это выглядело с новым потоком управления @-for?Взгляните на код ниже:

...

@Component({
...
template: `
<table>
...

<tbody>
@if (products$ | async; as products) {
@for (product of products; track product.title) {
<tr>
<td>{{ product.title }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.category }}</td>
</tr>
} @empty {
<p>No results yet!</p>
}
}
</tbody>
</table>
`,
...,
})
export class ProductsComponent {
products$ = inject(ProductService).getProducts();
}

Элементы ng-container | ng-template отсутствуют. Комбинация новых элементов @-for и @-empty устраняет необходимость проверки того, пуст ли список товаров, и какой шаблон выводить в зависимости от этого (сообщение по умолчанию или таблицу товаров). Следовательно, больше нет “императивной” проверки.

Блок @-if выполняет проверку на значение null, выдаваемое из асинхронного конвейера. Кроме того, новый синтаксис задействует функцию track, что повышает производительность. Таким образом, мы получаем меньше кода, который является чистым, более производительным и удобным для понимания, чтения и написания.


Когда я только начинал экспериментировать с новым потоком управления, я ожидал, что комбинация блоков @-empty и @-for будет работать “под капотом”, как в случае с оператором for await of в JavaScript. Я думал, что если данные для итерации загружаются асинхронно как результат наблюдаемой переменной или сигнала только для чтения, созданного функцией toSignal, то произойдет ожидание загрузки данных и затем будет принято решение, выводить ли блок @-empty или нет.

Таким образом, я полагал, что не нужно проверять наличие значения null при ожидании выдачи наблюдаемой переменной.

...

@Component({
...
template: `
<table>
...
<tbody>
// no @if check here...
@for (product of products$ | async; track product.title) {
<tr>
<td>{{ product.title }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.category }}</td>
</tr>
} @empty {
<p>No results yet!</p>
}
</tbody>
</table>
`,
...,
})
export class ProductsComponent {
products$ = inject(ProductService).getProducts();
}

А также при считывании сигнала только для чтения:

...

@Component({
...
template: `
<table>
...
<tbody>
// no @if check here...
@for (product of products(); track product.title) {
<tr>
<td>{{ product.title }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.category }}</td>
</tr>
} @empty {
<p>No results yet!</p>
}
</tbody>
</table>
`,
...,
})
export class ProductsComponent {
products = toSignal(this.productService.getProducts(), { initialValue: null });
}

Но как и оператор if/else в любом языке программирования, так и поток управления @-for в шаблонах работают с синхронизированной последовательностью значений.

В данном случае сначала выводится блок @-empty с сообщением “No results yet!” (“Пока нет результатов!”), а после получения данных с сервера выводится таблица товаров:

Когда тема для обсуждения была еще открыта, я оставил комментарий, в котором спрашивал, можно ли добавить опциональное условие в блок @-empty ({: empty} на момент написания вопроса), чтобы он мог дожидаться загрузки данных. Команда Angular заботится о своем сообществе и учитывает его мнение, так что, возможно, в ближайшем будущем они что-то сделают для реализации такого поведения.

Angular v17 официально находится в стадии релиз-кандидата. Смело знакомьтесь с его новыми возможностями.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ilir Beqiri: Declarative Loop Control Flow in Angular 17

Предыдущая статьяКлючевые вопросы для собеседования по Spring Boot в 2023 году. Часть 1
Следующая статьяПонимание шаблонов проектирования: шаблон “Строитель”