Как стать автором
Поиск
Написать публикацию
Обновить

Как сделать пошаговый гайд вашего приложения (если ваш проект на Angular)

Время на прочтение5 мин
Количество просмотров5.1K
Всем привет.

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

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

Архитектура


Архитектура данного компонента довольно проста.

У нас есть важный элемент в DOM-дереве, о котором мы хотим что-то рассказать пользователю, например, кнопка.

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

image

Для решения этой задачи нам поможет @angular/cdk. В своей прошлой статье я уже хвалил @angular/material которая зависит от CDK, для меня она по-прежнему остается примером качественно исполненных компонентов, созданных с использованием всех возможностей фреймворка.

Такие компоненты как меню, диалоговые окна, тултипы из библиотеки @angular/material сделаны с помощью компонента из CDK — Overlay.

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

Для начала установим библиотеки

npm i @angular/cdk @angular/material -S

После установки не забудьте добавить стили в style.css

@import '~@angular/cdk/overlay-prebuilt.css';
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

Теперь создадим новый модуль в нашем проекте:

ng generate library intro-lib

И сразу же cгенерируем шаблон для директивы:

ng generate directive intro-trigger

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

@Directive({
  selector: '[libIntroTrigger]'
})
export class IntroTriggerDirective {
  @Input() libIntroTrigger: string;

  constructor(private introLibService: IntroLibService, private elementRef: ElementRef) {}

  @HostListener('click') showGuideMessage(): void {
    this.introLibService.show$.emit([this.libIntroTrigger, this.elementRef]);
  }
}

Тут мы обращаемся к сервису который был так же сгенерирован при создании библиотеки, всю основную работу будет делать он.

Для начала объявим в сервисе новое свойство

show$ = new EventEmitter<[string, ElementRef]>();

Как мы уже видели эти значения нам будет присылать директива, в массиве первый элемент это описание, а второй — DOM-элемент который мы описываем (такое решение я выбрал для простоты примера).

@Injectable({
  providedIn: 'root'
})
export class IntroLibService {
  private overlayRef: OverlayRef;
  show$ = new EventEmitter<[string, ElementRef]>();

  constructor(private readonly overlay: Overlay, private readonly ngZone: NgZone, private readonly injector: Injector) {
    this.show$.subscribe(([description, elementRef]: [string, ElementRef]) => {
      this.attach(elementRef, description);
    });
  }
}

Обновим конструктор сервиса, добавив подписку на обновления от EventEmitter, функция attach будет получать обновления и создавать слои.

Для создания слоя нам понадобиться Overlay, Injector и NgZone.

Следующие действия можно разделить на несколько этапов:

  • Закрыть текущий оверлей (если он есть)
  • Создать PositionStrategy
  • Создать OverlayRef
  • Создать PortalInjector
  • Прикрепить компонент к слою

С первым пунктов ясно, для этого мы уже объявили свойство в сервисе. PositionStrategy — отвечает за то как наш слой будет позиционироваться в DOM — дереве.

Существует несколько готовых стратегий:

  1. FlexibleConnectedPositionStrategy
  2. GlobalPositionStrategy

Если простыми словами, то

FlexibleConnectedPositionStrategy — будет следить за конкретным элементом и в зависимости от конфигурации будет липнуть к нему при изменении размера браузера или скролле, явный пример использования выпадающие списки, меню.

GlobalPositionStrategy — как и говорится в названии, создается глобально, ему не нужен какой-либо элемент для работы, явный пример использования — модальные окна.

Добавим метод создания стратегии плавающего окна вокруг исследуемого элемента

{
 ...
private getPositionStrategy(elementRef: ElementRef): PositionStrategy {
    return this.overlay
      .position()
      .flexibleConnectedTo(elementRef)
      .withViewportMargin(8) // добавляем отступы от краев экрана
      .withGrowAfterOpen(true) // этот флаг помогает в случае если элемент может изменять размер (например, exspansion panel при открытии)
      .withPositions([ // задаем позиции расположения, при обновлениях позиции эта стратегия будет выбирать лучший вариант из перечисленного 
        {
          originX: 'start',
          originY: 'bottom',
          overlayX: 'start',
          overlayY: 'top'
        },
        {
          originX: 'start',
          originY: 'top',
          overlayX: 'start',
          overlayY: 'bottom'
        },
        {
          originX: 'end',
          originY: 'bottom',
          overlayX: 'end',
          overlayY: 'top'
        },
        {
          originX: 'end',
          originY: 'top',
          overlayX: 'end',
          overlayY: 'bottom'
        }
      ]);
  }
...
}

Добавим метод создания OverlayRef

createOverlay(elementRef: ElementRef): OverlayRef {
    const config = new OverlayConfig({
      positionStrategy: this.getPositionStrategy(elementRef),
      scrollStrategy: this.overlay.scrollStrategies.reposition()
    });

    return this.overlay.create(config);
  }

И добавим метод привязки нашего компонента к слою:

  attach(elementRef: ElementRef, description: string): void {
    if (this.overlayRef && this.overlayRef.hasAttached()) {
      this.overlayRef.dispose();
    }

    this.overlayRef = this.createOverlay(elementRef);

    const dataRef = this.ngZone.run(
      () => new DataRef(this.overlay, this.injector, this.overlayRef, elementRef, description)
    ); // тут следует отметить, что процедуры которые вызываются внутри конструктора, нужно совершить внутри зоны, иначе CD не узнает что что-то нужно перерисовать

    const injector = new PortalInjector(this.injector, new WeakMap([[DATA_TOKEN, dataRef]]));

    dataRef.overlayRef.attach(new ComponentPortal(IntroLibComponent, null, injector));
  }

Вот так у нас выглядит компонент показывающий сообщение

@Component({
  selector: 'lib-intro-lib',
  template: `
    <mat-card>
      <mat-card-content> {{ data.description }}</mat-card-content>
    </mat-card>
  `,
  styles: ['mat-card {width: 300px; margin: 32px;}']
})
export class IntroLibComponent {
  constructor(@Inject(DATA_TOKEN) public data: DataRef) {}
}

Про все что приведено в листингах уже рассказал, кроме DataRef.

DataRef — это простой класс который мы добавляем в инжектор для компонентов, фактически для передачи данных для отрисовки — например описание.

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

export class DataRef {
  shadowOverlayRef: OverlayRef;

  constructor(
    private overlay: Overlay,
    private injector: Injector,
    public overlayRef: OverlayRef,
    public elementRef: ElementRef,
    public description: string
  ) {
    const config = new OverlayConfig({
      positionStrategy: this.overlay.position().global(),
      scrollStrategy: this.overlay.scrollStrategies.block()
    });

    this.shadowOverlayRef = this.overlay.create(config);

    this.shadowOverlayRef.attach(
      new ComponentPortal(
        ShadowOverlayComponent,
        null,
        new PortalInjector(this.injector, new WeakMap([[DATA_TOKEN, this.elementRef]]))
      )
    );
  }
}


ShadowOverlayComponent — рисует компонент, и в инжекторе получает тот же токен, только с элементом, вокруг которого нужно сделать акцент.

Как я это реализовал, можно посмотреть в исходниках на github, отдельно на этом внимание акцентировать не хочется.

Только скажу, что там я рисую canvas на весь экран, рисую фигуру вокруг элемента, и заливаю контекст методом fill('evenodd');

Итого


Самое крутое, это то что @angular/cdk/overlay позволяет нам рисовать сколько угодно слоев. Они будут адаптивными и гибкими. Нам не нужно заботиться о том что изменится размер экрана, или элементы сместятся по каким то естественным причинам, overlay — подумает об этом за нас.

Мы поняли как работать со слоями, мы поняли что задача сделать пошаговый гайд — не такая и сложная.

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

Спасибо за внимание.
Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии0

Публикации

Ближайшие события