Появление нового функционала в Angular 16 и Angular 17
@for, @if, @empty и @switch
Angular предоставляет новые возможности для замены директив *ngFor
и *ngIf
. Допустим, у вас есть список курсов, и вы хотите отобразить их на экране. Все, что вам нужно сделать, это заиспользовать @for
и @if
, как вы используете директивы *ngFor
и *ngIf
, но с немного другим синтаксисом. Логика остается той же.
Использование @for, @empty
<div>
@for (course of courses; track course.id; let index = $index;
let count = $count; let first = $first; let last = $last) {
<app-listOfCourses
[course] = "course"
[index] = "index"
[class.is-first]= "first"
[class.is-last]= "last"
(courseSelected) = "onCourseSelected($event)" />
}
@empty {
<h1> No courses Found! </h1>
}
</div>
Приведенный выше код-пример представляет собой общий компонент, с помощью которого вы хотите отобразить свой список курсов с использованием дочернего компонента app-listOfCourses. Вы также можете использовать другие переменные: first
- означает первый элемент вашего массива. Count
- означает длину массива, track
- означает, что цикл отслеживает итерации с помощью идентификатора. Index
- это индекс текущего элемента. Для стилизации вы также можете использовать свойства first
, last
, even
, odd
, например, привязав их к class.is-last. Использование аналогичное, как мы ранее привыкли делать.
@empty
здесь предоставляет возможность отобразить ваш код в случае, если данные являются пустым массивом или пустым итерируемым объектом.
Свойство track
является обязательным. Это функция отслеживания, которая оптимизирует способ рендеринга DOM. Значение, которое вы указываете для свойства track
, должно быть уникальным для каждого элемента, чтобы Angular мог понять элементы в массиве при повторном рендеринге. Вы также можете написать свои собственные опции отслеживания в компоненте, а затем передать значение свойству track
. Можно сделать просто: track $index, если вы не хотите указывать идентификатор, но все же хотите избежать ошибки компиляции.
Использование @if
<div>
{{course.title}}
</div>
@if (course.id === 1) {
<h2> Course with ID 1</h2>
}
@else if (course.iconUrl) {
<img [src] = "course.iconUrl" alt="logo" width="300">
}
@else {
No image available
}
<div>
{{course.description}}
</div>
Использование @if
аналогично по функционалу *ngIf
, но своего рода упрощенная версия, поскольку в более сложных случаях этот синтаксис очень полезен для чтения кода, он упрощает код. Кроме того, он поддерживает условия else if
, вы можете использовать else if
без ограничений. Директива *ngIf
немного ограничена в случае else if
.
Использование @switch
// Ваш файл HTML, в котором вы пишете код, отображающий подробности каждого курса.
<div class= "course-category">
@switch(course.category) {
@case ("BEGINNER") {
<div class= "category">Beginner </div>
}
@case ("INTERMEDIATE") {
<div class= "category">Intermediate</div>
}
@case ("ADVANCED") {
<div class= "category">Advanced</div>
}
@default {
<div class= "category">Unknown</div>
}
}
</div>
Допустим, что у списка курсов есть категории, и вы хотите разделить их на 4 или 5 категорий и показать пользователю на экране. Вот где можно заиспользовать @switch
. Это хорошее решение, в котором использование case
улучшает читаемость кода. Логика такая же, как и у директивы ngSwitch
.
@Defer. Синтаксис частичной загрузки шаблона(ов)
Используется для решения проблем с производительностью, поэтому не всегда необходимо использовать эту функцию в приложениях Angular. Сценарии в которых вы хотите использовать @defer
, используются редко. Когда функциональность не всегда видна пользователю и мы не уверены, будет ли эта функциональность использоваться, задается вопрос: Зачем нам загружать код, который нам нужен для редких случаев?. Чтобы было более понятно, вот общие сценарии для применения @defer
:
- загрузка большого компонента только после прокрутки пользователя до определенной точки на странице;
- загрузка большого компонента только после щелчка пользователя по кнопке;
- предварительная загрузка большого компонента в фоновом режиме, пока пользователь читает страницу, чтобы он был готов к моменту, когда пользователь нажмет на кнопку.
Здесь используется синтаксис @defer для частичной загрузки шаблона. Представьте, что у вашего компонента есть некоторый код, который всегда отображается, но затем есть часть страницы, которая нужна только в определенных сценариях. В этой ненужной части содержится большой компонент, который мы хотим загрузить в фоновом режиме.
@Component({
selector: "huge-component",
template: ` <h1>huge-component is displayed...</h1> `,
})
export class ExampleComponent {}
Допустим, что у нас есть огромный компонент, который использует графические библиотеки и другие тяжелые зависимости для рендеринга. В таком случае мы хотим отложить загрузку этого большого компонента до тех пор, пока мы не будем на 100% уверены, что пользователь хочет его увидеть. Чтобы управлять этим поведением, оберните компонент в блок @defer
. Это сообщит Angular, что секция @defer
страницы должна быть упакована в отдельный файл JavaScript и загружаться отдельно. (Вы можете увидеть это поведение, если посмотрите на вывод сборки Angular CLI.)
@Component({
selector: "app",
template: `
<h2>Some content...</h2>
@defer {
<huge-component /> // наш огромный компонент, который мы создали выше.
}
`,
})
export class AppComponent {}
@defer
создаст дополнительный пакет, который содержит код для элемента huge-component. Это поможет ускорить загрузку приложения Angular, так как defer разделяет пакеты и приложение загружает huge-component только если блок defer сработает. Как же мы вызываем defer? Чтобы ответить на этот вопрос, давайте рассмотрим блоки @placeholder
, @loading
и @error
.
Использование @defer с @placeholder
@defer {
<huge-component />
}
@placeholder {
<initial-content />
}
Использование очень простое. Просто внесем небольшие изменения в нашем коде выше. Приложение будет отображать начальное содержимое до загрузки огромного компонента. Затем этот код начального содержимого будет заменен кодом, загруженным через блок @defer
. Таким образом, сначала начальное содержимое будет отображаться вместо блока defer, затем, если блок defer будет загружен и отрисован, начальное содержимое будет заменено на наш огромный компонент. Начальное содержимое должно быть только визуальным индикатором того, что скоро в это место будет помещено что-то, если вы используете директивы, пайпы и т.д. в начальном компоненте, эти вещи будут являться частью основного пакета.
У @placeholder
есть параметр "minimum", который используется для установки минимального времени, в течение которого блок placeholder будет показываться пользователю. Приложение будет отображать содержимое заполнителя до завершения заданного минимального времени.
Использование @defer с @loading
Блок @loading
используется для отображения некоторого содержимого во время загрузки JavaScript-пакета блока @defer
в фоновом режиме. Блок @loading
принимает два необязательных параметра: minimum и after:
- minimum - используется для указания минимального времени (в секундах или миллисекундах), в течение которого блок @loading будет отображаться пользователю.
- after - используется для указания времени ожидания, которое приложение должно пройти, прежде чем отображать индикатор @loading после начала процесса загрузки.
@placeholder
, будет отображаться до начала загрузки пакета. Но блок @loading
будет отрисован, когда загрузка пакета начинается. Вот в чем разница между этими двумя блоками.
Использование @defer с @error
Если что-то пошло не так при загрузке пакета, тогда мы можем использовать содержимое @error
для отображения ошибки пользователю с помощью блока @error
. Использование аналогично другим блокам:
@defer {
<huge-component />
} @error {
<error-message />
}
И так, разобрав некоторые примеры, нас ве же интересует, как работают триггеры @defer?
Существует два уровня управления данной ситуацией: триггер Prefetch, который контролирует загрузку пакета с бэкэнда, и триггер @defer, который контролирует отображение блока @defer пользователю. Таким образом, у нас есть два варианта.
- Мы можем использовать предопределенные триггеры.
- Мы можем написать свои собственные триггеры.
Предопределенные триггеры:
idle, viewport, interaction, hover, immediate, timer. Это предопределенные триггеры. Мы можем использовать их как триггеры для загрузки (prefetch triggers) и как триггеры @defer
. Для примеров использования их можно обратиться к документации по адресу angular.io/guide/defer.
Signals
Представьте, что у вас есть входные данные, которые отображаются пользователю в HTML, и вы изменяете эту переменную ввода, мутируя объект, массив или даже примитив. Например, переменная-счетчик ниже.
@Component({
selector: "app",
template: `
<h1>Counter value : {{ counter}}</h1>
<button (click)="increment()">Increment</button>
`,
})
export class AppComponent {
counter: number = 0 ;
increment() {
this.counter++;
}
}
Здесь Angular понимает, что переменная изменилась, сравнивая весь код. Если наш компонент большой, то это действительно плохо для оптимизации нашего приложения. Именно здесь нам могут помочь сигналы.
Сигналы имеют API для сообщения об изменениях данных в Angular, что позволяет фреймворку оптимизировать обнаружение изменений и перерисовку таким образом, что ранее было просто невозможно (на самом деле возможно с использованием системы обнаружения изменений, но эта система вызывает проблемы, если у вас есть большие и вложенные компоненты, когда дерево компонентов становится сложным). С помощью сигналов Angular сможет определить, какие части страницы нужно перерисовать, и перерисовывать только эти части. Можно сказать, что сигналы помогают улучшить производительность выполнения, избавляясь от Zone.js.
Версия примера с использованием сигналов выглядит вот так:
@Component({
selector: "app",
template: `
<h1>Counter value : {{ counter}}</h1>
<button (click)="increment()">Increment</button>
`,
})
export class AppComponent {
counter= signal(0);
increment() {
this.counter.set(this.counter()+1);
}
}
Сигналы представляют значение счетчика, которое начинается с 0. Мы можем получить значение сигнала, просто вызвав его как функцию: this.counter();
. Set()
здесь используется для изменения переменной. Также мы можем использовать это для обновления значения:
increment() {
this.counter.update(counter => counter + 1);
}
То же самое. this.counter() - это функция. counter => counter + 1 - это стрелочная функция. Основное преимущество заключается в том, что мы можем получать уведомления о изменении значения сигнала и затем выполнять какие-то действия в ответ на новое значение сигнала.
Также сигналы могут быть производными от других сигналов, например, от наблюдаемых объектов в rxjs. Для этого используйте computed
.
export class AppComponent {
counter= signal(0);
derivedCounter = computed(() => {
return this.counter() * 2;
})
increment() {
this.counter.set(this.counter()+1);
}
}
Теперь, когда у сигнала-исходника counter появляется новое значение, производный сигнал также автоматически обновляется. Angular теперь знает, что между этими двумя сигналами есть зависимость.
Сигналы с массивами и объектами в качестве значений
Массивы и объекты работают в основном так же, как и примитивные типы, но существуют вещи, на которые мы должны обратить внимание.
@Component(
selector: "app",
template: `
<h3>List value: {{list()}}</h3>
<h3>Object title: {{object().title}}</h3>
`)
export class AppComponent {
list = signal([
"a",
"b"
]);
object = signal({
id: 1,
title: "Title"
});
constructor() {
this.list().push("c");
this.object().title = "overwriting title";
}
}
Здесь мы использовали push и вручную изменили заголовок. Важно здесь отметить, что, в отличие от примитивного значения, ничто не мешает нам изменять содержимое массива напрямую, вызывая push для него, или изменять свойство объекта. Но так делать не нужно. Всегда должны использовать set или update для обновления значения сигналов. При этом мы предоставляем сигналам возможность обновляться и отображать эти изменения на экране для всех производных сигналов.
В данном материале я кратко рассказал об этих новых функциях. О них есть еще больше информации. Пожалуйста, обратитесь к официальной документации и другим ресурсам, чтобы узнать больше angular.io/guide/signals.