Примеры использования 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$ и отображения его окончательного значения.
Примеры доступны по ссылке
Это перевод оригинального материала