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

Angular CDK - создание настраиваемого диалогового окна 

Если вы создаете приложения с использованием Angular, вы, вероятно, знаете об удивительной библиотеке Angular Material. Это библиотека компонентов, реализующая спецификацию Material Design. Для многих команд и приложений, особенно тех, у кого нет ресурсов для проектирования, это фантастический инструмент для создания приложений с высококачественной системой дизайна. Я использовал его много раз с большим успехом.

Однако были времена, когда я хотел получить потрясающий опыт разработки Angular Material, но я не мог использовать дизайн материала, который он реализует, потому что у компании есть существующая система дизайна. Именно для таких ситуаций команда Angular создала Angular CDK, или «Component Dev Kit». Согласно их веб-сайту, CDK - это «набор примитивов поведения для создания компонентов пользовательского интерфейса». CDK - это фантастика, потому что он абстрагирует большую часть действительно сложной реализации поведения при создании компонентов пользовательского интерфейса.

Из-за всех этих преимуществ я стараюсь всегда максимально использовать CDK при написании приложений Angular. В этой серии постов я надеюсь изучить как можно больше частей CDK, чтобы помочь вам создать свою собственную высококачественную дизайн-систему. В этом посте я собираюсь специально поговорить о создании альтернативы MatDialog, поскольку это очень часто используемый сервис для создания модальных окон.

Dialog Service

В Angular Material сервис MatDialog позволяет вам передать компонент службе, которую она затем откроет в плавающем диалоговом окне с глобальным центром как по горизонтали, так и по вертикали. Очевидно, что этот диалог реализует спецификацию Material Design, включая анимацию при открытии и закрытии. Из-за этого мы хотим реализовать собственный дизайн, но эргономика сервиса MatDialog отличная. Таким образом, наша реализация, хотя и не совсем такая же, будет похожа и предоставит некоторые из тех же функций.

Дизайн API довольно прост, но при необходимости его можно расширить. У нас будет метод open, который принимает компонент Angular, который открывается в диалоговом окне. Мы также можем передавать данные в компонент, которые можно использовать при необходимости. Этот метод вернет ссылку на диалоговое окно, которое мы можем использовать, чтобы закрыть его программно или подписаться на него при его закрытии. Этот дизайн API прост и его легко расширять по мере необходимости, но он дает нам высокофункциональную диалоговую службу.

Вот демонстрация готового продукта со всеми упомянутыми выше функциями. Мы рассмотрим шаг за шагом, но вы можете просто сослаться на код здесь, если хотите.

Предпосылки

Прежде всего, нам нужно убедиться, что в нашем приложении есть Angular CDK. У вас есть два способа сделать это. Первое и наименьшее руководство - установить его вместе с Angular Material через предоставленную им схему Angular CLI. Обратите внимание, что это также установит и настроит Angular Material, тему и настроит стили. В некотором смысле это хорошо, потому что включает стили Angular CDK как часть темы Angular Material. Вы можете сделать это с помощью следующей команды:

ng add @angular/material

Если вы точно знаете, что никогда не захотите использовать что-либо из Angular Material, а хотите только CDK, вы можете установить его отдельно из npm. Обратите внимание, что вы должны установить тот же номер версии, который соответствует вашей версии Angular, например:

npm install --save @angular/cdk@12.2.13

Это не приведет к настройке каких-либо стилей, поэтому вам нужно будет правильно ссылаться на них в качестве схемы документации для каждой части, которую вы используете. Это метод, который я буду использовать в этих уроках, потому что я знаю, что мне не нужен Angular Material, поскольку в этом весь смысл этой серии. Итак, теперь, независимо от того, какой путь вы выбрали, у вас установлен Angular CDK и готов к работе!

CDK Overlay

Сначала позвольте мне объяснить, как работает концепция наложения CDK. Есть три части, которые работают вместе. Есть компонент, который мы хотим визуализировать, есть Portal, который представляет собой пакет CDK для визуализации динамического контента, такого как компонент, а затем есть Overlay, который представляет собой пакет CDK для открытия плавающих панелей на экране. В основном мы присоединяем компонент к ComponentPortal, а затем присоединяем этот портал к OverlayRef, который мы откроем.

Есть несколько способов использовать оверлей Angular CDK. Вы можете использовать его программно или даже как директивы по разметке. В нашей ситуации мы хотим использовать его программно, чтобы мы могли вызывать его из любого места через сервис в нашей логике. Итак, давайте начнем с создания простого сервиса Angular и отказа от базового API вместе с парой комментариев о том, что необходимо сделать.

import { ComponentType } from '@angular/cdk/overlay';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DialogService {
  constructor() {}

  open<T>(component: ComponentType<T>) {
    // 1. Create the overlay
    // 2. Attach component portal to the overlay
  }
}

Это самое начало нашего сервиса. Мы знаем, что нам нужен метод open, и мы знаем, что для его открытия требуется какой-то компонент. Вы заметите, что мы используем тип из ComponentType оверлейного пакета Angular CDK. Это тип, который позволяет нам получать любой компонент Angular, и это то, что передается в CDK при создании экземпляра компонента. Конечно, у нас также есть универсальный тип компонента <T>, через который мы проходим.

Создать наложение

Как мы уже упоминали выше, нам нужно сначала создать оверлей. Для создания наложения нам наиболее важно PositionStrategy. Это определяет, где на экране мы хотим открыть это наложение. Есть несколько вариантов, но в этом посте мы будем использовать расширение GlobalPositionStrategy. Это означает, что мы не будем прикреплять его к конкретному элементу. Мы также можем предоставить еще несколько дополнительных параметров конфигурации, которые мы и сделаем. Вот как мы создаем это наложение, внедряя класс Overlay в конструктор:

import { Overlay, ComponentType } from '@angular/cdk/overlay';
//...
export class DialogService {
  constructor(private overlay: Overlay) {}

  open<T>(component: ComponentType<T>) {
    // Globally centered position strategy
    const positionStrategy = this.overlay
      .position()
      .global()
      .centerHorizontally()
      .centerVertically();

    // Create the overlay with customizable options
    const overlayRef = this.overlay.create({
      positionStrategy,
      hasBackdrop: true,
      backdropClass: 'overlay-backdrop',
      panelClass: 'overlay-panel'
    });

    // Attach component portal to the overlay
  }
}

Мы сделали пару вещей. Во-первых, мы определили нашу позиционную стратегию. Мы заявили, что хотим глобальную стратегию и хотим расположить оверлей в центре экрана как по горизонтали, так и по вертикали. Вы также можете позиционировать свой оверлей (аналогично абсолютному позиционированию), задав ему верхнее, левое, правое или нижнее значение. Это может быть полезно, если вы хотите открыть боковую панель или нижний лист. Поскольку мы просто делаем стандартное модальное окно, мы центрируем его на экране.

Мы также определяем некоторую информацию о панели и фоне. Сначала мы определяем, что нам нужен фон для этого модального окна, а также предоставляем для него класс фона. Здесь мы можем определить, как мы хотим стилизовать фон, который я буду стилизовать с помощью темного полупрозрачного фона. Мы также предоставляем класс панели, который будет применен к родительской «панели», в которой мы будем рендерить наш компонент. Я просто применил базовый стиль, чтобы сделать фон белым и иметь небольшой отступ. Вы можете увидеть мои стили, которые я предоставил src/styles.scss.

Создать портал компонентов

Затем нам нужно создать наш ComponentPortal, который мы затем прикрепим к наложению. Это довольно просто, и мы делаем это так:

import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
//...
export class DialogService {
  constructor(private overlay: Overlay) {}

  open<T>(component: ComponentType<T>) {
    // Globally centered position strategy
    // ...

    // Create the overlay with customizable options
    const overlayRef = this.overlay.create({
      // ...
    });

    // Attach component portal to the overlay
    const portal = new ComponentPortal(component);
    overlayRef.attach(portal);
  }
}

На этом этапе мы можем успешно открыть этот диалог, если действительно вызовем этот метод и передадим ему компонент. Хотя мы могли бы просто оставить наш сервис вот так, он определенно не соответствует обычным сценариям использования, с которыми мы могли бы работать MatDialog. Мы хотим иметь возможность возвращать ссылку на диалог, чтобы мы могли программно закрыть оверлей или подписаться на него при закрытии оверлея. Итак, давайте добавим это к нашей реализации.

Справка по диалогу

Создадим простой класс DialogRef. Он должен принимать OverlayRef, который мы можем использовать для закрытия оверлея, и он должен иметь rxjs Subject, чтобы мы могли подписаться, когда оверлей закрывается. Итак, давайте реализуем этот простой класс:

import { OverlayRef } from '@angular/cdk/overlay';
import { Subject, Observable } from 'rxjs';

/**
 * A reference to the dialog itself.
 * Can be injected into the component added to the overlay and then used to close itself.
 */
export class DialogRef {
  private afterClosedSubject = new Subject<any>();

  constructor(private overlayRef: OverlayRef) {}

  /**
   * Closes the overlay. You can optionally provide a result.
   */
  public close(result?: any) {
    this.overlayRef.dispose();
    this.afterClosedSubject.next(result);
    this.afterClosedSubject.complete();
  }

  /**
   * An Observable that notifies when the overlay has closed
   */
  public afterClosed(): Observable<any> {
    return this.afterClosedSubject.asObservable();
  }
}

Теперь нам нужно добавить это в наш метод open, чтобы мы могли создать эту ссылку и вернуть ее из метода при создании. Итак, давайте поместим это сюда:

import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';
//...
export class DialogService {
  constructor(private overlay: Overlay) {}

  open<T>(component: ComponentType<T>): DialogRef {
    // Globally centered position strategy
    // ...

    // Create the overlay with customizable options
    const overlayRef = this.overlay.create({
      // ...
    });

    // Create dialogRef to return
    const dialogRef = new DialogRef(overlayRef);

    // Attach component portal to the overlay
    // ...

    return dialogRef;
  }
}

Это очень полезно для потребителей этого API, так как они могут получить доступ к диалоговому окну. Но как насчет того компонента, который мы открываем? Мы хотим, чтобы компонент в наложении мог закрываться сам. Итак, как мы можем передать dialogRef? Что ж, для этого нам нужно создать инжектор, который мы передадим на портал компонентов. Это позволит нам затем внедрить dialogRef в наш компонент. Сделать это довольно просто, можно так:

import { Injectable, Injector } from '@angular/core';
import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';
//...
export class DialogService {
  constructor(private overlay: Overlay, private injector: Injector) {}

  open<T>(component: ComponentType<T>): DialogRef {
    // Globally centered position strategy
    // ...

    // Create the overlay with customizable options
    const overlayRef = this.overlay.create({
      // ...
    });

    // Create dialogRef to return
    const dialogRef = new DialogRef(overlayRef);

    // Create injector to be able to reference the DialogRef from within the component
    const injector = Injector.create({
      parent: this.injector,
      providers: [{ provide: DialogRef, useValue: dialogRef }]
    });

    // Attach component portal to the overlay
    const portal = new ComponentPortal(component, null, injector);
    overlayRef.attach(portal);

    return dialogRef;
  }
}

Теперь, когда мы предоставили инжектор для портала компонентов, мы сможем очень просто ввести dialogRef в наш компонент:

@Component({
  // ...
})
export class LoginComponent {
  constructor(private dialogRef: DialogRef) {}

  close() {
    this.dialogRef.close();
  }
}

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

Данные диалога

Чтобы передать данные в компонент, мы будем использовать тот же метод, что и наш dialogRef. Однако в этом случае нам нужно определить наш собственный токен внедрения для системы внедрения зависимостей. Начнем с того, что сделаем это в новом файле dialog-tokens.ts. Это будет очень простой файл.

import { InjectionToken } from '@angular/core';

export const DIALOG_DATA = new InjectionToken<any>('DIALOG_DATA');

Теперь, когда мы создали очень простой токен инъекции, мы можем добавить его в наш инжектор. Нам также необходимо обновить наш метод open, чтобы он принимал необязательные данные для передачи компоненту. В рамках этого мы определим интерфейс DialogConfig, у которого есть опциональная data. Причина, по которой мы делаем этот объект конфигурации таким, заключается в том, что его легко расширить позже, если вы захотите, например, разрешить настройку параметров наложения.

import { Injectable, Injector } from '@angular/core';
import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';

export interface DialogConfig {
  data?: any;
}

//...
export class DialogService {
  constructor(private overlay: Overlay, private injector: Injector) {}

  open<T>(component: ComponentType<T>, config?: DialogConfig): DialogRef {
    // Globally centered position strategy
    // ...

    // Create the overlay with customizable options
    // ...

    // Create dialogRef to return
    // ...

    // Create injector to be able to reference the DialogRef from within the component
    const injector = Injector.create({
      parent: this.injector,
      providers: [
        { provide: DialogRef, useValue: dialogRef },
        { provide: DIALOG_DATA, useValue: config?.data }
      ]
    });

    // Attach component portal to the overlay
    // ...

    return dialogRef;
  }
}

Заключение

Теперь, когда мы создали этот сервис с возможностью многократного использования, мы можем открыть любой компонент, который захотим, в хорошо отцентрированном модальном окне! При желании мы можем предоставить ему данные, и мы можем ссылаться на этот диалог извне, чтобы закрыть его, если захотим, или подписаться на его закрытие и реагировать на это по мере необходимости. Мы могли бы пойти дальше, например, определив наши собственные переходы и анимации для модального входа и выхода. Или мы могли бы легко передать параметр конфигурации, чтобы изменить стратегию позиционирования, чтобы она открывалась как боковая панель вместо модального окна по центру. Есть много способов, которыми вы можете настроить это, чтобы получить именно то, что вы хотите, и это полностью под вашим контролем, а не заблокировано в диалоговом окне дизайна материалов и взаимодействиях.

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

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

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

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