
Начиная с версии 21.2 Angular внедрил поддержку чистых JS функций в html шаблонах. Теперь можно инлайнить функции без необходимости определения их в классе компонента. Фича, на мой взгляд, довольно противоречивая, поэтому давайте разбираться.
В качестве примера мы будем использовать вот такой компонент со списком героев.
import { Component, signal, WritableSignal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeroesList } from '../heroes-list/heroes-list';
export interface Hero {
id: number;
name: string;
lastName: string;
nickname: string;
email: string;
}
@Component({
selector: 'app-heroes',
imports: [HeroesList, CommonModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.scss',
standalone: true
})
export class HeroesComponent {
heroes: WritableSignal<Hero[]> = signal<Hero[]>([
{
id: 1,
name: 'Peter',
lastName: 'Parker',
nickname: 'Spider man',
email: 'peter@parker.com'
},
{
id: 2,
name: 'Tony',
lastName: 'Stark',
nickname: 'Iron man',
email: 'tony@stark.com'
},
{
id: 3,
name: 'Stephen',
lastName: 'Strange',
nickname: 'Doctor Strange',
email: 'stephen@strange.com',
},
{
id: 4,
name: 'Natasha',
lastName: 'Romanoff',
nickname: 'Black widow',
email: 'natasha@romanoff.com',
},
{
id: 5,
name: 'Bruce',
lastName: 'Banner',
nickname:'Hulk',
email: 'bruce@banner.com',
}
]);
selectedHeroId: WritableSignal<number> = signal<number>(1);
}
Для начала выведем список всех героев на экран. Здесь нет ничего особенного, обычная работа с сигналами.
<ul>
@for (hero of heroes(); track $index) {
<li>
{{ `${hero.name} ${hero.lastName} - ${hero.email}` }}
</li>
}
</ul>
Результат
- Peter Parker - peter@parker.com
- Tony Stark - tony@stark.com
- Stephen Strange - stephen@strange.com
- Natasha Romanoff - natasha@romanoff.com
- Bruce Banner - bruce@banner.com
Теперь давайте посмотрим более подробную информацию по каждому из героев. При помощи кнопок Вперед/Назад мы будем перемещаться по списку героев и вытаскивать данные по каждому из них используя его id. Хранить выбранный id мы будем в сигнале selectedHeroId.
И вот здесь уже начинаются отличия. Раньше для того, чтобы изменить значение сигнала нам пришлось бы создать в классе компонента 2 метода и вызывать их по клику на соответствующую кнопку.
<button (click)="prevHero()">Назад</button>
<button (click)="nextHero()">Вперед</button>
prevHero(): void {
this.selectedHeroId.update((count: number) => count === 1 ? this.heroes().length : count - 1)
}
nextHero(): void {
this.selectedHeroId.update((count: number) => count === this.heroes().length ? 1 : count + 1)
}
Сейчас мы можем делать это прямо в шаблоне.
<div class="hero-navigation">
<button (click)="selectedHeroId.update(count => count === 1 ? heroes().length : count - 1)">Назад</button>
<div class="hero-name">
{{ heroes()[selectedHeroId() - 1].name + ' ' + heroes()[selectedHeroId() - 1].lastName }}
</div>
<button (click)="selectedHeroId.update(count => count === heroes().length ? 1 : count + 1)">Вперед</button>
</div>
<div style="hero-info">
{{ heroes()[selectedHeroId() - 1] | json }}
</div>
Результат

Далее, покажем только тех героев, у которых совпадают первые буквы их имени и фамилии.
<div>
{{ heroes().filter(hero => hero.name.charAt(0).toLowerCase() === hero.lastName.charAt(0).toLowerCase()).map(hero => `${hero.name} ${hero.lastName}` ).join(', ') }}
</div>
Результат
Peter Parker, Stephen Strange, Bruce Banner
Как видим, фильтрация работает.
Попробуем найти героя по домену его почты.
<div>
{{ heroes().find(hero => hero.email.endsWith('stark.com'))?.name }}
</div>
Результат
Tony
А как насчет передачи стрелочной функции в качестве значения инпута? Для теста создадим еще один компонент, который выводит отсортированный по имени список героев.
import { Component, computed, input, InputSignal, Signal } from '@angular/core';
import { Hero } from '../heroes/heroes.component';
@Component({
selector: 'app-heroes-list',
template: `
<ul>
@for (hero of sortedHeroes(); track $index;) {
<li>
{{ hero.name + ' ' + hero.lastName }}
</li>
}
</ul>
`,
standalone: true
})
export class HeroesList {
heroes: InputSignal<Hero[]> = input.required<Hero[]>();
sortFn: InputSignal<(a: Hero, b: Hero) => number> = input.required<(a: Hero, b: Hero) => number>();
sortedHeroes: Signal<Hero[]> = computed(() => [...this.heroes()].sort(this.sortFn()));
}
И добавим этот компонент в наш основной шаблон.
<div>
<app-heroes-list [heroes]="heroes()" [sortFn]="(a, b) => a.name.localeCompare(b.name)"></app-heroes-list>
</div>
Результат
- Bruce Banner
- Natasha Romanoff
- Peter Parker
- Stephen Strange
- Tony Stark
Сортировка работает.
Мы также можем работать и с литералами объектов. Для этого их надо обернуть в круглые скобки. Если этого не сделать компилятор вернет ошибку. Все дело в том, что в JavaScript фигурные скобки после стрелки обозначают тело функции, но никак не объект.
Неправильно.
{{ heroes().map(item => { name: 'Super ' + item.nickname, email: item.email }) }}

Правильно.
<ul>
@for (hero of heroes().map(item => ({ name: 'Super ' + item.nickname, email: item.email })); track $index) {
<li>
{{ `${ hero.name } - ${ hero.email }` }}
</li>
}
</ul>
Результат
- Super Spider man - peter@parker.com
- Super Iron man - tony@stark.com
- Super Doctor Strange - stephen@strange.com
- Super Black widow - natasha@romanoff.com
- Super Hulk - bruce@banner.com
Что еще НЕЛЬЗЯ сделать в шаблоне.
- Как указано на скриншоте ошибки выше, нельзя работать с многострочными функциями.
{{ heroes().map(item => {return { name: 'Super ' + item.nickname, email: item.email }}) }}
- Функция не должна возвращать другую функцию. Данный кусок кода не приведет к ошибке, но и не отработает как положено. Скомпилированный код будет выведен в виде текста.
{{ () => heroes().find(hero => hero.email.endsWith('stark.com'))?.name }}
Результат
() => { let tmp_0_0; return (tmp_0_0 = ctx.heroes().find((hero) => hero.email.endsWith("stark.com"))) == null ? null : tmp_0_0.name; }
- Также нельзя использовать пайпы в теле функции. Пайп это синтаксис шаблонов Angular и чистый JS их не поддерживает. Вот так делать нельзя.
{{ heroes().find(hero => hero.email.endsWith('StaRk.com' | lowercase ))?.name }}
Но можно применить пайп к самой функции (по сути к ее результату).
{{ heroes().find(hero => hero.email.endsWith('stark.com'))?.name | uppercase }}
Результат
TONY
Я очень люблю Angular, но данный апдейт вызывает у меня вопросы. С одной стороны, применение функций в шаблонах в некоторых случаях может быть довольно удобным. И даже привести к сокращению какого-то количества кода. С другой стороны, чем меньше логики в шаблонах, тем лучше. Плюс, уже множество раз обсуждалось, что использование функций в шаблонах негативно влияет на производительность приложения. И что вместо функций лучше использовать пайпы.
Хотелось бы услышать ваше мнение по этому поводу. Как вы думаете, стоит ли использовать функции в шаблонах? Какие вы видите аргументы за и против?