Что нового в Angular 17.2?
Давайте рассмотрим все новые функции, введенные в последнем релизе Angular 17.2, а так же изучим альтернативы декораторам @ViewChild
, @ViewChildren
, @ContentChild
и @ContentChildren
, которые заменяют декораторы с теми же названиями. Разберем, почему эти декораторы делают существующие хуки жизненного цикла AfterContentInit
и AfterViewInit
необязательными, а также рассмотрим новый API model()
, который позволит нам осуществлять двустороннюю привязку данных с использованием сигналов.
@ViewChild с использованием Signal-based Queries
Начнем с обсуждения новой альтернативы декоратору @ViewChild.
Существующий декоратор @ViewChild позволяет получить ссылку на дочерний элемент в шаблоне:
@Component({ selector: "signals-demo", template: ` <p>Parent counter {{ parentCounter }}</p> <signal-counter [(count)]="parentCounter" /> `, standalone: true, imports: [CounterComponent], }) export class SignalsDemoComponent implements AfterViewInit { parentCounter = 0; @ViewChild(SignalCounter) counter: SignalCounter; ngAfterViewInit() { console.log( "counter component:", this.counter); } }
В этом примере используется компонент SignalCounter
, и используется ссылка на него в компоненте.
До Angular 17.2 запрос создавался при помощи @ViewChild:
@ViewChild(SignalCounter) counter: SignalCounter;
Теперь есть возможность Angular 17.2 выполнить то же самое, но с использованием сигналов. Для этого просто уберём использование декоратора @ViewChild
и AfterViewInit
.
@Component({ selector: "signals-demo", template: ` <p>Parent counter {{ parentCounter }}</p> <signal-counter #counter [(count)]="parentCounter" /> `, standalone: true, imports: [CounterComponent], }) export class SignalsDemoComponent { parentCounter = 0; counter = viewChild(CounterComponent); constructor() { effect(() => { console.log("counter component:", this.counter()); }); } }
Этот подход работает так же, но новый API выглядит намного проще.
Так же использование метода жизненного цикла AfterViewInit
заменено использованием простого сигнала effect()
. Стоит обратить внимание, что сигнал счётчика теперь является сигналом типа Signal
от CounterComponent
. Для работы с ним можно использовать любой функционал в Signals API
.
Рассмотрим работу с параметрами декоратора @ViewChild
. Команда разработчиков предусмотрела все ранее заложенные возможности, присутствует объект options, который можно передать. Например, можно передать свойство "read", как и в стандартном декораторе @ViewChild
:
counter = viewChild(CounterComponent, { read: true, });
Так же можно получить доступ к представлению, используя переменную шаблона. Например, у нас есть переменная шаблона #counter и можно передать имя переменной:
counter = viewChild("counter");
Если мы хотим сделать это сигнал viewChild
обязательным, то можно воспользоваться Required Signals.
counter = viewChild.required("counter");
@ViewChildren с использованием signal-based API
Рассмотрим еще один запрос шаблона — viewChildren
.
Предположим, у нас есть компонент, который использует несколько дочерних компонентов того же типа в своем шаблоне. Angular дает возможность запросить все дочерние компоненты одновременно с помощью нового запроса viewChildren
:
@Component({ selector: "signals-demo", template: ` <p>Parent counter {{ parentCounter }}</p> <signal-counter [(count)] /> <signal-counter [(count)] /> <signal-counter [(count)] /> `, standalone: true, imports: [CounterComponent], }) export class SignalsDemoComponent { parentCounter = 0; counters = viewChildren(CounterComponent); constructor() { effect(() => { console.log("counters component:", this.counters()); }); } }
Стоит обратить внимание, что нам больше не нужен перехват жизненного цикла, простой эффект решает все задачи.
Альтернативы @ContentChild и @ContentChildren с использованием сигналов
Существуют также новые сигнальные альтернативы для существующих декораторов @ContentChild
и @ContentChildren
, которые работают точно так же, как объяснено для @ViewChild
и @ViewChildren
.
Двусторонняя связка с использованием model()
До версии Angular 17.2 использовался традиционный синтаксис двустороннего связывания данных на основе [(ngModel)], команда Angular пересмотрела этот подход с использованием нового API:
@Component({ selector: "signal-counter", template: ` <div> <div>Counter value: {{ count() }}</div> <button (click)="onIncrement()">Increment</button> </div> `, standalone: true, }) export class CounterComponent { count = model(0); onIncrement() { this.count.update((val) => val + 1); } }
Из примера видно, что теперь мы просто получаем сигнал, который представляет собой счетчик. Каждый раз, когда мы нажимаем на кнопку, мы увеличиваем значение счетчика. Если мы проверим тип этой переменной count
, мы увидим, что это ModelSignal<number>
.
Это специальный тип сигнала для записи, который позволяет нам выполнять двустороннее связывание данных с точки зрения родительского компонента.
Итак, если вернуться к родительскому компоненту:
@Component({ selector: "signals-demo", template: ` <p>Parent counter {{ parentCounter }}</p> <signal-counter [(count)]="parentCounter" /> `, standalone: true, imports: [CounterComponent], }) export class SignalsDemoComponent { parentCounter = 0; }
Мы видим, что теперь применяется синтаксис двустороннего связывания [()] к свойству count:
<signal-counter [(count)]="parentCounter" />
Переменная parentCounter
теперь автоматически синхронизирована со переменной counter внутри SignalCounter
.
Связь работает в обе стороны. Так что если я инициализирую parentCounter
значением 100, то значение переменной counter внутри SignalCounter
также будет инициализировано значением 100.
Затем, если мы начнем увеличивать счетчик, значения будут увеличиваться на единицу, начиная с 100, 101 и 102, как ожидалось.
Это перевод оригинального материала