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

Хотелось бы услышать ваше мнение по этому поводу. Как вы думаете, стоит ли использовать функции в шаблонах? Какие вы видите аргументы за и против?