How to make a step-by-step guide of your application (if your project is on Angular)

Hello everyone.

Not so long ago, the next sprint ended, and I had some time to make for my users not the most necessary, but at the same time interesting feature - an interactive guide for working with our application.

There are a lot of ready-made solutions on the Internet - all of them can certainly be suitable for this task, but we will see how to do it yourself.

Architecture


The architecture of this component is quite simple.

We have an important element in the DOM tree that we want to tell the user something, for example, a button.

We need to draw a darkened layer around this element, switching attention to it.
You need to draw a card next to this element with an important message.

image

To solve this problem, @ angular / cdk will help us . In my last article, I already praised @ angular / material, which depends on the CDK, for me it still remains an example of high-quality components created using all the features of the framework.

Components such as menus, dialog boxes, toolbars from the @ angular / material librarymade using the component from CDK - Overlay.

The simple interface of this component allows you to quickly create a layer on top of our application, which will independently adjust to changes in screen size and scrolling. As you already understood, using this component to solve our problem becomes very simple.

First, install the libraries

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

After installation, do not forget to add styles to style.css

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

Now create a new module in our project:

ng generate library intro-lib

And immediately generate a template for the directive:

ng generate directive intro-trigger

This directive will act as a trigger for our future guide, will listen to clicks on an element and highlight it on the page.

@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]);
  }
}

Here we turn to the service that was also generated when the library was created, all the main work will be done by him.

First, declare a new property in the service

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

As we have seen these values, the directive will send us, in the array the first element is the description, and the second is the DOM element that we describe (I chose this solution for simplicity of example).

@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);
    });
  }
}

Update the service designer by adding a subscription to updates from EventEmitter, the attach function will receive updates and create layers.

To create the layer we need Overlay, Injector and NgZone.

The following actions can be divided into several stages:

  • Close current overlay (if any)
  • Create PositionStrategy
  • Create OverlayRef
  • Create PortalInjector
  • Attach a component to a layer

With the first points it is clear, for this we have already declared the property in the service. PositionStrategy - is responsible for how our layer will be positioned in the DOM tree.

There are several ready-made strategies:

  1. FlexibleConnectedPositionStrategy
  2. GlobalPositionStrategy

In simple words, then

FlexibleConnectedPositionStrategy - will follow a specific element and, depending on the configuration, it will stick to it when changing the size of the browser or scroll, a clear example of use is drop-down lists, menus.

GlobalPositionStrategy - as the name says, it is created globally, it does not need any element to work, an obvious example of use is modal windows.

Add a method to create a floating window strategy around the element under investigation.

{
 ...
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'
        }
      ]);
  }
...
}

Add a method to create OverlayRef

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

    return this.overlay.create(config);
  }

And add a method to bind our component to the layer:

  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));
  }

This is how the component showing the message looks like

@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) {}
}

Already told about everything that is resulted in listings, except DataRef.

DataRef is a simple class that we add to the injector for components, in fact, to transfer data for rendering - for example, a description.

Also, in it I decided to draw another layer to darken and highlight the element. In this case, we will already use the global layer creation strategy.

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 - draws a component, and in the injector receives the same token, only with the element around which you need to emphasize.

How I implemented this, you can see in the sources on github , I do not want to focus on this separately.

I’ll just say that there I draw the canvas full screen, draw a shape around the element, and fill the context with the fill ('evenodd') method;

Total


The coolest thing is that @ angular / cdk / overlay allows us to draw as many layers as we like. They will be adaptive and flexible. We do not need to worry about changing the screen size, or the elements will shift for some natural reasons, overlay will think about it for us.

We understood how to work with layers, we realized that the task of creating a step-by-step guide is not so difficult.

You can modify the library by adding the ability to switch between elements, exit the view mode and a number of other corner cases.

Thank you for the attention.

Source: https://habr.com/ru/post/undefined/


All Articles