February 23

Что нового в 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, как ожидалось.

Это перевод оригинального материала