Angular AoT Custom Decorator Error encountered resolving symbol values statically

后端 未结 2 1143
伪装坚强ぢ
伪装坚强ぢ 2020-12-30 12:15

I created a decorator to help me with handling desktop/mobile events

import { HostListener } from \'@angular/core\';

type MobileAwareEventName =
  | \'click         


        
相关标签:
2条回答
  • 2020-12-30 12:36

    This means exactly what the error says. Function calls aren't supported in a place where you're doing them. The extension of the behaviour of Angular built-in decorators isn't supported.

    AOT compilation (triggered by --prod option) allows to statically analyze existing code and replace some pieces with expected results of their evaluation. Dynamic behaviour in these places means that AOT cannot be used for the application, which is a major drawback for the application.

    If you need custom behaviour, HostListener shouldn't be used. Since it basically sets up a listener on the element, this should be done manually with renderer provider, which is preferable Angular abstraction over DOM.

    This can be solved with custom decorator:

    interface IMobileAwareDirective {
      injector: Injector;
      ngOnInit?: Function;
      ngOnDestroy?: Function;
    }
    
    export function MobileAwareListener(eventName) {
      return (classProto: IMobileAwareDirective, prop, decorator) => {
        if (!classProto['_maPatched']) {
          classProto['_maPatched'] = true;
          classProto['_maEventsMap'] = [...(classProto['_maEventsMap'] || [])];
    
          const ngOnInitUnpatched = classProto.ngOnInit;
          classProto.ngOnInit = function(this: IMobileAwareDirective) {
            const renderer2 = this.injector.get(Renderer2);
            const elementRef = this.injector.get(ElementRef);
            const eventNameRegex = /^(?:(window|document|body):|)(.+)/;
    
            for (const { eventName, listener } of classProto['_maEventsMap']) {
              // parse targets
              const [, eventTarget, eventTargetedName] = eventName.match(eventNameRegex);
              const unlisten = renderer2.listen(
                eventTarget || elementRef.nativeElement,
                eventTargetedName,
                listener.bind(this)
              );
              // save unlisten callbacks for ngOnDestroy
              // ...
            }
    
            if (ngOnInitUnpatched)
              return ngOnInitUnpatched.call(this);
          }
          // patch classProto.ngOnDestroy if it exists to remove a listener
          // ...
        }
    
        // eventName can be tampered here or later in patched ngOnInit
        classProto['_maEventsMap'].push({ eventName, listener:  classProto[prop] });
      }
    }
    

    And used like:

    export class FooComponent {
      constructor(public injector: Injector) {}
    
      @MobileAwareListener('clickstart')
      bar(e) {
        console.log('bar', e);
      }
    
      @MobileAwareListener('body:clickstart')
      baz(e) {
        console.log('baz', e);
      }  
    }
    

    IMobileAwareDirective interface plays important role here. It forces a class to have injector property and this way has access to its injector and own dependencies (including ElementRef, which is local and obviously not available on root injector). This convention is the preferable way for decorators to interact with class instance dependencies. class ... implements IMobileAwareDirective can also be added for expressiveness.

    MobileAwareListener differs from HostListener in that the latter accepts a list of argument names (including magical $event), while the former just accepts event object and is bound to class instance. This can be changed when needed.

    Here is a demo.

    There are several concerns that should be addressed additionally here. Event listeners should be removed in ngOnDestroy. There may be potential problems with class inheritance, this needs to be additionally tested.

    0 讨论(0)
  • 2020-12-30 12:41

    A full implementation of estus answer. This works with inheritance. The only downside is that still requires the component to include injector in the constructor.

    Full code on StackBlitz

    import { ElementRef, Injector, Renderer2 } from '@angular/core';
    
    function normalizeEventName(eventName: string) {
      return typeof document.ontouchstart !== 'undefined'
        ? eventName
            .replace('clickstart', 'touchstart')
            .replace('clickmove', 'touchmove')
            .replace('clickend', 'touchend')
        : eventName
            .replace('clickstart', 'mousedown')
            .replace('clickmove', 'mousemove')
            .replace('clickend', 'mouseup');
    }
    
    
    interface MobileAwareEventComponent {
      _macSubscribedEvents?: any[];
      injector: Injector;
      ngOnDestroy?: () => void;
      ngOnInit?: () => void;
    }
    
    export function MobileAwareHostListener(eventName: string) {
      return (classProto: MobileAwareEventComponent, prop: string) => {
        classProto._macSubscribedEvents = [];
    
        const ngOnInitUnmodified = classProto.ngOnInit;
        classProto.ngOnInit = function(this: MobileAwareEventComponent) {
          if (ngOnInitUnmodified) {
            ngOnInitUnmodified.call(this);
          }
    
          const renderer = this.injector.get(Renderer2) as Renderer2;
          const elementRef = this.injector.get(ElementRef) as ElementRef;
    
          const eventNameRegex = /^(?:(window|document|body):|)(.+)/;
          const [, eventTarget, eventTargetedName] = eventName.match(eventNameRegex);
    
          const unlisten = renderer.listen(
            eventTarget || elementRef.nativeElement,
            normalizeEventName(eventTargetedName),
            classProto[prop].bind(this),
          );
    
          classProto._macSubscribedEvents.push(unlisten);
        };
    
        const ngOnDestroyUnmodified = classProto.ngOnDestroy;
        classProto.ngOnDestroy = function(this: MobileAwareEventComponent) {
          if (ngOnDestroyUnmodified) {
            ngOnDestroyUnmodified.call(this);
          }
    
          classProto._macSubscribedEvents.forEach((unlisten) => unlisten());
        };
      };
    }
    
    0 讨论(0)
提交回复
热议问题