We pump work with events in Angular

Once upon a time, I wrote an article about working with EventManager in Angular . In it, I talked about how you can save the usual syntax for event subscriptions, while avoiding unnecessary triggers of checking changes on frequent and sensitive events.


However, the method I described is cumbersome and difficult to understand. It's time to rewrite the filtering on decorators.



Brief replay


For those who have not read and do not want to read the previous article, a summary of the problem:


  1. Angular allows declaratively subscribing to events in the template ( (eventName)) and through decorators ( @HostListener(‘eventName’)).
  2. With a change validation strategy, OnPushAngular will run a validation if an event occurs that we subscribed to in this way.
  3. Events such as scroll, mousemove, dragtriggered very often. In practice, you only need to respond to some of them (for example, when the user scrolled the container to the end - load new elements).
  4. Angular handles event processing EventManagerusing the provided EventManagerPlugins.
  5. Angular , .

. . .



Angular (keydown.ctrl.enter ).


, EventManager. . Angular , EVENT_MANAGER_PLUGINS . , , . addEventListener , , , , , . .


preventDefault stopPropagation. , , , :


@Injectable()
export class StopEventPlugin {
  supports(event: string): boolean {
    return event.split('.').includes('stop');
  }

  addEventListener(
    element: HTMLElement, 
    event: string, 
    handler: Function
  ): Function {
    const wrapped = (event: Event) => {
      event.stopPropagation();
      handler(event);
    };

    return this.manager.addEventListener(
      element,
      event
        .split('.')
        .filter(v => v !== 'stop')
        .join('.'),
      wrapped,
    );
  }
}

. , :


  1. Angular, .
  2. .
  3. .

, NgZone :


@Injectable()
export class SilentEventPlugin {
  supports(event: string): boolean {
    return event.split('.').includes('silent');
  }

  addEventListener(
    element: HTMLElement,
    event: string,
    handler: Function
  ): Function {
    return this.manager.getZone().runOutsideAngular(() =>
      this.manager.addEventListener(
        element,
        event
          .split('.')
          .filter(v => v !== 'silent')
          .join('.'),
        handler,
      ),
    );
  }
}

, .



, -. /, this.


, , . — - , , . $event @HostListener. :


export function shouldCall<T>(
  predicate: Predicate<T>
): MethodDecorator {
  return (_target, _key, desc: PropertyDescriptor) => {
    const {value} = desc;

    desc.value = function(this: T, ...args: any[]) {
      if (predicate.apply(this, args)) {
        value.apply(this, args);
      }
    };
  };
}

. — - Angular, .


Angular 10 Ivy , markDirty(this). , - NgZone. . , . , NgZone , :


@Injectable()
export class ZoneEventPlugin {
  supports(event: string): boolean {
    return event.split('.').includes('init');
  }

  addEventListener(
    _element: HTMLElement,
    _event: string, 
    handler: Function
  ): Function {
    const zone = this.manager.getZone();
    const subscription = zone.onStable.subscribe(() => {
      subscription.unsubscribe();
      handler(zone);
    });

    return () => {};
  }
}

.init , ( , ). @HostListener(‘prop.init’, [‘$event’]) :


export function shouldCall<T>(
  predicate: Predicate<T>
): MethodDecorator {
  return (_, key, desc: PropertyDescriptor) => {
    const {value} = desc;

    desc.value = function() {
      const zone = arguments[0] as NgZone;

      Object.defineProperty(this, key, {
        value(this: T, ...args: any[]) {
          if (predicate.apply(this, args)) {
            zone.run(() => {
              value.apply(this, args);
            });
          }
        },
      });
    };
  };
}

, . , . Ivy.




, , :
https://stackblitz.com/edit/angular-event-filter-decorator


, , , . , , . async Observable :


<p *ngFor="let item of service.items$ | async">{{item}}</p>

, :


export function scrolledToBottom(
   {scrollTop, scrollHeight, clientHeight}: HTMLElement
): boolean {
  return scrollTop >= scrollHeight - clientHeight - 20;
}

@Component({
  selector: 'awesome-component',
  templateUrl: './awesome-component.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AwesomeComponent {
  constructor(@Inject(Service) readonly service: Service) {}

  @HostListener('scroll.silent', ['$event.currentTarget'])
  @HostListener('init.onScroll', ['$event'])
  @shouldCall(scrolledToBottom)
  onScroll() {
    this.service.loadMore();
  }
}

. :
https://stackblitz.com/edit/angular-event-filters-scroll


, . CustomEvent, . .


(1 gzip) open-source- @tinkoff/ng-event-filters. Angular 10 2.0.0, markDirty(this), Angular 4.



npm-


-, open source, ? Angular Open-source Library Starter, . CI, , , CHANGELOG, .

All Articles