DevGang
Авторизоваться

Овладейте искусством проецирования контента Angular 

Когда дело доходит до создания настраиваемых компонентов в Angular, думайте об этом как о рецепте вашего утреннего кофе: у вас есть основа (@Input()@Output()) и дополнительные компоненты (*ngIf) для создания идеального напитка. Однако чем больше начинки вы добавляете в свой утренний кофе, тем больше ваш компонент становится слишком тесным для вашей базовой бизнес-логики.

В идеале задача компонента — обеспечивать только взаимодействие с пользователем.

В этом сообщении блога мы отодвигаем слои, чтобы раскрыть его неиспользованный потенциал для создания Angular гибких и настраиваемых компонентов.

Начнем с Angular проецирования контента.

1. Что такое проекция контента Angular? (ng-content)

Проекция контента — это шаблон, в котором вы вставляете или проецируете контент, который хотите использовать внутри другого компонента.

Чтобы проиллюстрировать эту концепцию, рассмотрим следующий пример:

@Component({
  standalone: true,
  selector: 'app-greeny',
  template: `
    <p>Welcome to Greeny Land 🍀!</p>
    <ng-content></ng-content>
  `,
})
export class GreenyComponent {}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, GreenyComponent],
  template: `
    <app-greeny>
      <p>I'm a Seeding 🌱, a new beginning and a fresh start.</p>
    </app-greeny>
  `,
})
export class App {}

bootstrapApplication(App);

Добавляя элементы <ng-content></ng-content> в GreenyComponent шаблон, вы обеспечиваете динамическое включение и плавное смешивание содержимого родительского компонента.

Welcome to Greeny Land 🍀!
I'm a Seeding 🌱, a new beginning and a fresh start.

2. Многослотовая проекция контента

Angular расширяет проекцию контента за пределы своей базовой концепции, вводя проекцию контента с несколькими слотами, которая позволяет вставлять контент в определенные назначенные слоты внутри компонента, обеспечивая более точный контроль над настройкой.

Давайте рассмотрим эту расширенную функцию на практическом примере с использованием Pokémon API:

pokemon.component.ts
@Component({
  standalone: true,
  selector: 'app-pokemon',
  template: `
    <div class="header-wrapper">
      <ng-content select=".pokemon-header"></ng-content>
    </div>
    <div class="detail-wrapper">
      <ng-content select=".pokemon-detail"></ng-content>
    </div>
  `,
})
export class PokemonComponent {}

Мы определили PokemonComponent поддержку проекций контента на несколько слотов, указав конкретный select атрибут (селектор) для каждого <ng-content> слота:

  • pokemon-header
  • pokemon-detail

Angular поддерживает селекторы для любой комбинации имени тега, атрибута, класса CSS и псевдокласса :not.

standard-pokemon.component.ts

Давайте создадим StandardPokemonComponent, чтобы использовать проекцию контента с несколькими слотами:

@Component({
  selector: 'app-standard-pokemon',
  standalone: true,
  imports: [PokemonComponent, TitleCasePipe],
  template: `
    <div class="standard">
      <app-pokemon [class]="pokemon.type">
        <div class="pokemon-header">
          <div class="number"><small>#{{pokemon.id}}</small></div>
          <img [src]="pokemon.image" [alt]="pokemon.name" />
        </div>
        <div class="pokemon-detail">
          <h3>{{pokemon.name | titlecase}}</h3>
          <small>Type: {{pokemon.type}}</small>
        </div>
      </app-pokemon>
    </div>
  `,
  styleUrls: ['./standard-pokemon.component.scss'],
})
export class StandardPokemonComponent {
  @Input() pokemon: any;
}

Чтобы проецировать содержимое в соответствующий слот, вам просто нужно определить <div> элемент, содержащий класс pokemon-header или pokemon-detail.

pokemon-header будет включать индекс и изображение покемона,

<div class="pokemon-header">
  <div class="number"><small>#{{pokemon.id}}</small></div>
  <img [src]="pokemon.image" [alt]="pokemon.name" />
</div>

В то время как pokemon-detail будет содержать имя и тип покемона.

<div class="pokemon-detail">
  <h3>{{pokemon.name | titlecase}}</h3>
  <small>Type: {{pokemon.type}}</small>
</div>

Кроме того, мы отрисуем цвет фона в зависимости от типа покемона.

А теперь оживим наших покемонов:

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, StandardPokemonComponent],
  template: `
    <h1 class="title">Pokémon Evolution</h1>

    <div class="container">
      <ng-container *ngIf="pokemons$ | async as pokemonList">
        <ng-container *ngFor="let pokemon of pokemons">
          <app-standard-pokemon [pokemon]="pokemon"></app-standard-pokemon>
        </ng-container>
      </ng-container>
    </div>
  `,
  styles: [...]
})
export class App {
  pokemons$ = inject(PokemonService).getPokemons();
}

Ссылка на StackBlitz

На данный момент мы узнали, как работает многослотовая проекция контента Angular. Однако какова в этом практическая польза? Почему бы нам просто не скомпоновать все внутри PokemonComponent? Давайте рассмотрим эти вопросы в следующем разделе.

3. Пришло время сделать Content Projection блестящим

Предположим, мы получили новое требование от нашего начальника. Вместо того, чтобы просто поддерживать стандартных покемонов, он хочет, чтобы мы приняли современный стиль покемонов, а именно:

Давайте сначала посмотрим, как реализуется ModernPokemonComponent:

@Component({
  selector: 'app-modern-pokemon',
  standalone: true,
  imports: [PokemonComponent, TitleCasePipe],
  template: `
    <div class="modern">
      <app-pokemon>
        <div class="pokemon-header">
          <div class="number"><small>{{pokemon.id}}</small></div>
          <img pokemon-image [src]="pokemon.artwork" [alt]="pokemon.name" />
        </div>
        <div class="pokemon-detail">
          <div class="headline">
            <h3 class="name">{{pokemon.name | titlecase}}</h3>
            <div class="type-wrapper">
              <small class="type">{{pokemon.type | titlecase}}</small>
              <small>{{getTypeEmoji(pokemon.type)}}</small>
            </div>
          </div>
          <div class="stat-wrapper">
            <div class="stat">
              <span class="stat-number">{{pokemon.attack}}</span>
              <span class="stat-name">Attack</span>
            </div>
            <div class="stat">
              <span class="stat-number">{{pokemon.defense}}</span>
              <span class="stat-name">Defense</span>
            </div>
            <div class="stat">
              <span class="stat-number">{{pokemon.speed}}</span>
              <span class="stat-name">Speed</span>
            </div>
          </div>
        </div>
      </app-pokemon>
    </div>
  `,
  styleUrls: ['./modern-pokemon.component.scss'],
})
export class ModernPokemonComponent {
  @Input() pokemon: any;

  getTypeEmoji(type: string): string {
    ...
  }
}

Заголовок Pokémon остается неизменным, а раздел сведений о Pokémon состоит из двух основных компонентов: headline и stats

  • headline
<div class="headline">
  <h3 class="name">{{pokemon.name | titlecase}}</h3>
  <div class="type-wrapper">
    <small class="type">{{pokemon.type | titlecase}}</small>
    <small>{{getTypeEmoji(pokemon.type)}}</small>
  </div>
</div>
  • stats
<div class="stat-wrapper">
  <div class="stat">
    <span class="stat-number">{{pokemon.attack}}</span>
    <span class="stat-name">Attack</span>
  </div>
  <div class="stat">
    <span class="stat-number">{{pokemon.defense}}</span>
    <span class="stat-name">Defense</span>
  </div>
  <div class="stat">
    <span class="stat-number">{{pokemon.speed}}</span>
    <span class="stat-name">Speed</span>
  </div>
</div>

Как видите, все pokemon-detail совершенно разные. Однако для поддержки нам не нужно вносить какие-либо изменения в PokemonComponent. Все обрабатывается отдельно внутри ModernPokemonComponent. Таким образом, мы можем гарантировать, что StandardPokemonComponent останется неизменным без каких-либо последствий.

Благодаря Content Projection мы можем разместить две разные карты покемонов совершенно разных стилей. Другими словами, Content Projection позволяет нам придерживаться принципа разделения ответственности в отношении пользовательского интерфейса. Пользовательские интерфейсы динамически управляются на основе конкретной бизнес-логики каждого компонента (StandardPokemonComponent и ModernPokemonComponent)

Ссылка на StackBlitz

Последняя мысль

Спасибо, что дошли до конца! Content Projection — это фундаментальный инструмент, который помогает нам создавать гибкие компоненты. Мы продолжим эту серию, углубившись в еще один мощный инструмент Angular — *ngTemplateOutlet.

Источник:

#JavaScript #Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу