Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular AoT Custom Decorator Error encountered resolving symbol values statically

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

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

type MobileAwareEventName =
  | 'clickstart'
  | 'clickmove'
  | 'clickend'
  | 'document:clickstart'
  | 'document:clickmove'
  | 'document:clickend'
  | 'window:clickstart'
  | 'window:clickmove'
  | 'window:clickend';

export const 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');
};

export const MobileAwareHostListener = (
  eventName: MobileAwareEventName,
  args?: string[],
) => {
  return HostListener(normalizeEventName(eventName), args);
};

The problem with that is when I try to compile with --prod, I get the following error

typescript error
Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing
the function or lambda with a reference to an exported function (position 26:40 in the original .ts file),
resolving symbol MobileAwareHostListener in
.../event-listener.decorator.ts, resolving symbol HomePage in
.../home.ts

Error: The Angular AoT build failed. See the issues above

What is wrong? How can I fix that?

like image 253
BrunoLM Avatar asked Feb 19 '18 20:02

BrunoLM


2 Answers

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());
    };
  };
}
like image 22
BrunoLM Avatar answered Sep 29 '22 04:09

BrunoLM


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.

like image 80
Estus Flask Avatar answered Sep 29 '22 03:09

Estus Flask