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?
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());
};
};
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With