Well, it turned out to be simple, but not straightforward, so I'm going to post the solution I found here.
What I want to do is saving a usable document.addEventListener
in a global object before Zone.JS patches the hell out of the browser's native objects.
This must be done before the loading of polyfills, because it's in there that Zone is loaded. And, it must not be done in plain code in polyfills.ts
, because the import
statements are processed before any other code.
So I need a file zone-config.ts
in the same folder of polyfills.ts
. In the latter an extra import is needed:
import './zone-config'; // <- adding this line to the file, before...
// Zone JS is required by default for Angular itself.
import 'zone.js/dist/zone'; // Included with Angular CLI.
In zone-config.ts
I do the sleight-of-hand:
(function(global) {
// Save native handlers in the window.unpatched object.
if (global.unpatched) return;
if (global.Zone) {
throw Error('Zone already running: cannot access native listeners');
}
global.unpatched = {
windowAddEventListener: window.addEventListener.bind(window),
windowRemoveEventListener: window.removeEventListener.bind(window),
documentAddEventListener: document.addEventListener.bind(document),
documentRemoveEventListener: document.removeEventListener.bind(document)
};
// Disable Zone.JS patching of WebSocket -- UNRELATED TO THIS QUESTION
const propsArray = global.__Zone_ignore_on_properties || (global.__Zone_ignore_on_properties = []);
propsArray.push({ target: WebSocket.prototype, ignoreProperties: ['close', 'error', 'open', 'message'] });
// disable addEventListener
const evtsArray = global.__zone_symbol__BLACK_LISTED_EVENTS || (global.__zone_symbol__BLACK_LISTED_EVENTS = []);
evtsArray.push('message');
})(<any>window);
Now I have a window.unpatched
object available, that allows me to opt out of Zone.JS completely, for very specific tasks, like the IdleService
I was working on:
import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
/* This is the object initialized in zone-config.ts */
const unpatched: {
windowAddEventListener: typeof window.addEventListener;
windowRemoveEventListener: typeof window.removeEventListener;
documentAddEventListener: typeof document.addEventListener;
documentRemoveEventListener: typeof document.removeEventListener;
} = window['unpatched'];
/** Manages event handlers used to detect User activity on the page,
* either via mouse or keyboard events. */
@Injectable({
providedIn: 'root'
})
export class IdleService {
private _idleForSecond$ = new BehaviorSubject<number>(0);
constructor(zone: NgZone) {
const timerCallback = (idleFromSeconds: number): void => this._idleForSecond$.next(idleFromSeconds);
this.registerIdleCallback(timerCallback);
}
private registerIdleCallback(callback: (idleFromSeconds: number) => void) {
let idleFromSeconds = 0;
const resetCounter = () => (idleFromSeconds = -1);
// runs entirely out of Zone.JS
unpatched.documentAddEventListener('mousemove', resetCounter);
unpatched.documentAddEventListener('keypress', resetCounter);
unpatched.documentAddEventListener('touchstart', resetCounter);
// runs in the Zone normally
window.setInterval(() => callback(++idleFromSeconds), 1000);
}
/** Returns an observable that emits when the user's inactivity time
* surpasses a certain threshold.
*
* @param seconds The minimum inactivity time in seconds.
*/
byAtLeast(seconds: number): Observable<number> {
return this._idleForSecond$.pipe(filter(idleTime => idleTime >= seconds));
}
/** Returns an observable that emits every second the number of seconds since the
* ladt user's activity.
*/
by(): Observable<number> {
return this._idleForSecond$.asObservable();
}
}
Now the stackTrace in the (idleFromSeconds = -1)
callback is empty as desired.
Hope this can be of help to someone else: it's not complicated, but having all the bits in place was a bit of a chore.