Примеры использования Signals в Angular
Сигналы имеют важное значение в разработке на Angular, поскольку открывают новые возможности. Например, Signal input
упрощает создание вычисляемых сигналов, обеспечивая более эффективные процессы разработки. .
В течение этого поста проиллюстрируем эти концепции на практических примерах, используя выдуманный API.
Обязательный Signal Input
// pokemon.component.ts import { ChangeDetectionStrategy, Component, input } from '@angular/core'; @Component({ selector: 'app-pokemon', standalone: true, imports: [PokemonCardComponent], ], template: ` <p>Pokemon id: {{ id() }}</p> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); }
Компонент PokemonComponent
имеет обязательный входной сигнал id
, который ожидает числовое значение. Это значение отображается встроенным шаблоном.
// main.ts @Component({ selector: 'app-root', standalone: true, imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule], template: ` <div> <app-pokemon [id]="25" /> <app-pokemon [id]="52" /> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class App {}
Вычисляемый сигнал, основанный на входящем
// pokemon.componen.ts import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; @Component({ selector: 'app-pokemon', standalone: true, template: ` <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); nextId = computed(() => this.id() + 1);
nextId
- это вычисляемый сигнал, который увеличивает входной сигнал id
на 1.
Входящий сигнал со значением по умолчанию
// pokemon.componen.ts import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; @Component({ selector: 'app-pokemon', standalone: true, template: ` <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p> <p [style.background]="bgColor()"> Background color: {{ bgColor() }} </p> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); bgColor = input('cyan', { alias: 'backgroundColor' }); nextId = computed(() => this.id() + 1); }
В компоненте объявлен входной сигнал с начальным значением cyan
, и алиасом backgroundColor
. Когда у компонента нет входного backgroundColor,
применяется значение cyan
.
// main.ts @Component({ selector: 'app-root', standalone: true, imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule], template: ` <div> <app-pokemon [id]="25" /> <app-pokemon [id]="52" backgroundColor="yellow" /> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class App {}
Измененный Signal Input
// pokemon.component.ts import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; @Component({ selector: 'app-pokemon', standalone: true, template: ` <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p> <p [style.background]="bgColor()"> Background color: {{ bgColor() }} </p> <p>Transformed: {{ text() }}</p> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); bgColor = input('cyan', { alias: 'backgroundColor' }); text = input<string, string>('', { alias: 'transformedText', transform: (v) => `transformed ${v}!`, }); nextId = computed(() => this.id() + 1); }
В компоненте объявлен еще один входной сигнал с пустой строкой, которой присваивается псевдоним transformedText
, а также transform-функция.
// main.ts @Component({ selector: 'app-root', standalone: true, imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule], template: ` <div> <app-pokemon [id]="25" transformedText="red" /> <app-pokemon [id]="52" backgroundColor="yellow" transformedText="green" /> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class App {}
Signal input + API in effect()
// get-pokemon.util.ts import { HttpClient } from '@angular/common/http'; import { assertInInjectionContext, inject } from '@angular/core'; import { PokemonType } from '../types/pokemon.type'; export function getPokemonFn() { assertInInjectionContext(getPokemonFn); const httpClient = inject(HttpClient); const URL = `https://pokeapi.co/api/v2/pokemon`; return function (id: number) { return httpClient.get<PokemonType>(`${URL}/${id}/`) } }
// pokemon.componen.ts @Component({ selector: 'app-pokemon', standalone: true, imports: [PokemonCardComponent], template: ` ... omitted unrelated codes ... <div class="container"> @if (pokemon(); as pokemon) { <app-pokemon-card [pokemon]="pokemon" /> } @if (nextPokemon(); as pokemon) { <app-pokemon-card [pokemon]="pokemon" /> } </div> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); nextId = computed(() => this.id() + 1); getPokemon = getPokemonFn(); pokemon = signal<PokemonType | undefined>(undefined); nextPokemon = signal<PokemonType | undefined>(undefined); constructor() { effect((onCleanup) => { const sub = this.getPokemon(this.id()) .subscribe((pokemon) => this.pokemon.set(pokemon)); const sub2 = this.getPokemon(this.nextId()) .subscribe((pokemon) => this.nextPokemon.set(pokemon)); onCleanup(() => { sub.unsubscribe(); sub2.unsubscribe(); }); }); } }
В функции effect()
обратный вызов вызывает функцию this.getPokemon
для получения объекта Pokemon
по входному сигналу id
и вычисленному сигналу nextId
соответственно. Когда подписывается первый Observable
, объект Pokemon
устанавливается в сигнал pokemon
. Когда подписывается второй Observable
, объект Pokemon
устанавливается в сигнал nextPokemon
. Кроме того, функция onCleanup
отписывает подписки для предотвращения утечки памяти. В шаблоне кода новый контрольный поток проверяет, определены ли сигналы перед передачей данных в PokemonCardComponent для отрисовки.
Signal input + withComponentInputBinding
// app.config.ts import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), provideRouter(routes, withComponentInputBinding()) ] };
Функция provideRouter
имеет функцию withComponentInputBinding()
, поэтому данные роута преобразуются в сигналы ввода в компоненте.
// app.route.ts import { Routes } from '@angular/router'; export const routes: Routes = [ { path: 'pokemons/pidgeotto', loadComponent: () => import('./pokemons/pokemon/pokemon.component').then((m) => m.PokemonComponent), data: { id: 17, backgroundColor: 'magenta', transformedText: 'magenta', } }, ];
В пути маршрута pokemons/pidgeotto
я передаются данные маршрута: id
, backgroundColor
и tranformedText
. Поскольку включена функция withComponentInputBinding()
, данные маршрута автоматически преобразуются в сигналы ввода: id
, bgColor
и text
.
// main.ts @Component({ selector: 'app-root', standalone: true, imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule], template: ` <div> <h2>Signal inputs with route data</h2> <ul> <li><a [routerLink]="['/pokemons/pidgeotto']">pidgeotto</a></li> </ul> <router-outlet /> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class App {}
Signal input + Computed signals + Host property
// font-size.directive.ts import { computed, Directive, input } from '@angular/core'; @Directive({ selector: '[appFontSize]', standalone: true, host: { '[style.font-size.px]': 'size()', '[style.font-weight]': 'fontWeight()', '[style.font-style]': 'fontStyle()', '[style.color]': 'color()' }, }) export class FontSizeDirective { size = input(14); shouldDoStyling = computed(() => this.size() > 20 && this.size() <= 36); fontWeight = computed(() => this.shouldDoStyling() ? 'bold' : 'normal'); fontStyle = computed(() => this.shouldDoStyling() ? 'italic' : 'normal'); color = computed(() => this.shouldDoStyling() ? 'blue' : 'black'); }
Создана директива для изменения размера, насыщенности, стиля шрифта и цвета. Всякий раз, когда изменяется размер, shouldDoStyling
определяет, должны ли применяться стили CSS.
// pokemon.component.ts @Component({ selector: 'app-pokemon', standalone: true, imports: [PokemonCardComponent], hostDirectives: [ { directive: FontSizeDirective, inputs: ['size'], } ], template: ` <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p> <p [style.background]="bgColor()"> Background color: {{ bgColor() }} </p> <p>Transformed: {{ text() }}</p> // omitted because the code is irrelevant <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); bgColor = input('cyan', { alias: 'backgroundColor' }); text = input<string, string>('', { alias: 'transformedText', transform: (v) => `transformed ${v}!`, }); nextId = computed(() => this.id() + 1); // omitted because the code is irrelevant }
PokemonComponent
регистрирует директиву FontSizeDirective
как хост-директиву с входным параметром size
. Стили CSS применяются к элементам абзаца, когда компонент App присваивает значение входному параметру size.
// main.ts @Component({ selector: 'app-root', standalone: true, imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule], template: ` <div> <label for="size"> <span>Size: </span> <input type="number" id="size" name="size" [ngModel]="size()" (ngModelChange)="size.set($event)" min="8" /> </label> <app-pokemon [id]="25" transformedText="red" [size]="size()" /> <app-pokemon [id]="52" backgroundColor="yellow" transformedText="green" [size]="size()" /> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class App { size = signal(16); }
Остался еще один пример, который касается взаимодействия сигнального ввода и RxJS.
// pokemon.component.ts @Component({ selector: 'app-pokemon', standalone: true, imports: [PokemonCardComponent, AsyncPipe], hostDirectives: [ { directive: FontSizeDirective, inputs: ['size'], } ], template: ` <p>Observable: {{ value$ | async }}</p> <hr /> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class PokemonComponent { id = input.required<number>(); bgColor = input('cyan', { alias: 'backgroundColor' }); text = input<string, string>('', { alias: 'transformedText', transform: (v) => `transformed ${v}!`, }); value$ = toObservable(this.bgColor) .pipe( combineLatestWith(toObservable(this.text)), map(([color, color2]) => `${color}|${color2}`), map((color) => `@@${color.toUpperCase()}@@`), ); }
value$
- это Observable
, который объединяет toObservable(this.bgColor)
и toObservable(this.text)
для преобразования сигнальных входов в новый текст. Во встроенном шаблоне я использую асинхронную операцию для разрешения value$
и отображения его окончательного значения.
Примеры доступны по ссылке
Это перевод оригинального материала