I created a decorator to help me with handling desktop/mobile events
import { HostListener } from \'@angular/core\';
type MobileAwareEventName =
| \'click
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.
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());
};
};
}