
Начиная с версии 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, но данный апдейт вызывает у меня вопросы. С одной стороны, применение функций в шаблонах в некоторых случаях может быть довольно удобным. И даже привести к сокращению какого-то количества кода. С другой стороны, чем меньше логики в шаблонах, тем лучше. Плюс, уже множество раз обсуждалось, что использование функций в шаблонах негативно влияет на производительность приложения. И что вместо функций лучше использовать пайпы.
Хотелось бы услышать ваше мнение по этому поводу. Как вы думаете, стоит ли использовать функции в шаблонах? Какие вы видите аргументы за и против?
