Skip to content

Commit

Permalink
feat(interruptsource): allow interrupts to use an ssr option
Browse files Browse the repository at this point in the history
ssr option instructs interrupt sources to ignore targets
derived from global context such as window or document so
they can be safely used in ssr/universal apps.

If you are using ssr/universal, do not use
DEFAULT_INTERRUPTSOURCES. Instead, use
`createDefaultInterruptSources({ssr: !isPlatformBrowser(platformId) })`.
 See docs or https://stackoverflow.com/a/46893433/64750
for information on how to use isPlatformBrowser.

Fixes #77, #115
  • Loading branch information
grbsk committed Jul 23, 2019
1 parent db0148b commit 239be3a
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 17 deletions.
4 changes: 2 additions & 2 deletions projects/core/src/lib/defaultinterruptsources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export function createDefaultInterruptSources(
'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll',
options
),
new StorageInterruptSource()
new StorageInterruptSource(options)
];
}

export const DEFAULT_INTERRUPTSOURCES: any[] = createDefaultInterruptSources();
export const DEFAULT_INTERRUPTSOURCES = createDefaultInterruptSources();
13 changes: 13 additions & 0 deletions projects/core/src/lib/documentinterruptsource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ describe('core/DocumentInterruptSource', () => {
source.detach();
}));

it('does not emit events when ssr option is true', fakeAsync(() => {
const source = new DocumentInterruptSource('click', { ssr: true });
spyOn(source.onInterrupt, 'emit').and.callThrough();
source.attach();

const expected = new Event('click');
document.documentElement.dispatchEvent(expected);

expect(source.onInterrupt.emit).not.toHaveBeenCalled();

source.detach();
}));

it('does not emit onInterrupt event when detached and event is fired', fakeAsync(() => {
const source = new DocumentInterruptSource('click');
spyOn(source.onInterrupt, 'emit').and.callThrough();
Expand Down
11 changes: 10 additions & 1 deletion projects/core/src/lib/documentinterruptsource.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

import {
EventTargetInterruptOptions,
EventTargetInterruptSource
Expand All @@ -8,7 +11,13 @@ import {
*/
export class DocumentInterruptSource extends EventTargetInterruptSource {
constructor(events: string, options?: number | EventTargetInterruptOptions) {
super(document.documentElement, events, options);
const target =
options && (options as EventTargetInterruptOptions).ssr
? null
: document.documentElement;

options = !options || typeof options === 'number' ? {} : options;
super(target, events, options);
}

/*
Expand Down
49 changes: 42 additions & 7 deletions projects/core/src/lib/eventtargetinterruptsource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,36 @@ describe('core/EventTargetInterruptSource', () => {
expect(source.onInterrupt.emit).not.toHaveBeenCalled();
}));

it('does not emit onInterrupt event when ssr is true', fakeAsync(() => {
const source = new EventTargetInterruptSource(document.body, 'click', {
ssr: true
});
spyOn(source.onInterrupt, 'emit').and.callThrough();

source.attach();

const expected = new Event('click');
document.body.dispatchEvent(expected);

expect(source.onInterrupt.emit).not.toHaveBeenCalled();

source.detach();
}));

it('does not emit onInterrupt event when target is null', fakeAsync(() => {
const source = new EventTargetInterruptSource(null, 'click');
spyOn(source.onInterrupt, 'emit').and.callThrough();

source.attach();

const expected = new Event('click');
document.body.dispatchEvent(expected);

expect(source.onInterrupt.emit).not.toHaveBeenCalled();

source.detach();
}));

it('should throttle target events using the specified throttleDelay value', fakeAsync(() => {
const source = new EventTargetInterruptSource(document.body, 'click', 500);
spyOn(source.onInterrupt, 'emit').and.callThrough();
Expand Down Expand Up @@ -97,43 +127,48 @@ describe('core/EventTargetInterruptSource', () => {
it('should set default options', () => {
const target = {};
const source = new EventTargetInterruptSource(target, 'click');
const { throttleDelay, passive } = source.options;
const { throttleDelay, passive, ssr } = source.options;

expect(passive).toBeFalsy();
expect(throttleDelay).toBe(500);
expect(ssr).toBeFalsy();
});

it('should set passive flag', () => {
const target = {};
const source = new EventTargetInterruptSource(target, 'click', {
passive: true
});
const { throttleDelay, passive } = source.options;
const { throttleDelay, passive, ssr: isBrowser } = source.options;

expect(passive).toBeTruthy();
expect(throttleDelay).toBe(500);
expect(isBrowser).toBeFalsy();
});

it('should set throttleDelay', () => {
const target = {};
const source = new EventTargetInterruptSource(target, 'click', {
throttleDelay: 1000
});
const { throttleDelay, passive } = source.options;
const { throttleDelay, passive, ssr } = source.options;

expect(passive).toBeFalsy();
expect(throttleDelay).toBe(1000);
expect(ssr).toBeFalsy();
});

it('should set both options', () => {
it('should set all options', () => {
const target = {};
const source = new EventTargetInterruptSource(target, 'click', {
throttleDelay: 1000,
passive: true
passive: true,
ssr: true,
throttleDelay: 1000
});
const { throttleDelay, passive } = source.options;
const { throttleDelay, passive, ssr } = source.options;

expect(passive).toBeTruthy();
expect(throttleDelay).toBe(1000);
expect(ssr).toBeTruthy();
});
});
25 changes: 21 additions & 4 deletions projects/core/src/lib/eventtargetinterruptsource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export interface EventTargetInterruptOptions {
* Note: you need to detect if the browser supports passive listeners, and only set this to true if it does.
*/
passive?: boolean;

/**
* Whether or not the app is running on the server side or non-browser context where access
* to browser globals and user input is not possible.
*/
ssr?: boolean;
}

const defaultThrottleDelay = 500;
Expand All @@ -30,6 +36,7 @@ export class EventTargetInterruptSource extends InterruptSource {
private eventSubscription: Subscription = new Subscription();
protected throttleDelay: number;
protected passive: boolean;
protected ssr: boolean;

constructor(
protected target: any,
Expand All @@ -39,12 +46,13 @@ export class EventTargetInterruptSource extends InterruptSource {
super(null, null);

if (typeof options === 'number') {
options = { throttleDelay: options, passive: false };
options = { throttleDelay: options, passive: false, ssr: false };
}

options = options || {
throttleDelay: defaultThrottleDelay,
passive: false
passive: false,
ssr: false,
throttleDelay: defaultThrottleDelay
};

if (options.throttleDelay === undefined || options.throttleDelay === null) {
Expand All @@ -53,6 +61,11 @@ export class EventTargetInterruptSource extends InterruptSource {

this.throttleDelay = options.throttleDelay;
this.passive = !!options.passive;
this.ssr = !!options.ssr;

if (this.ssr || !target) {
return;
}

const opts = this.passive ? { passive: true } : null;
const fromEvents = events
Expand Down Expand Up @@ -89,6 +102,10 @@ export class EventTargetInterruptSource extends InterruptSource {
* @return The current option values.
*/
get options(): EventTargetInterruptOptions {
return { throttleDelay: this.throttleDelay, passive: this.passive };
return {
passive: this.passive,
ssr: this.ssr,
throttleDelay: this.throttleDelay
};
}
}
14 changes: 14 additions & 0 deletions projects/core/src/lib/storageinterruptsource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,18 @@ describe('core/StorageInterruptSource', () => {

source.detach();
}));

it('does not emit onInterrupt event when ssr is true', fakeAsync(() => {
const source = new StorageInterruptSource({ ssr: true });
spyOn(source.onInterrupt, 'emit').and.callThrough();

source.attach();

const expected = new StorageEvent('storage');
window.dispatchEvent(expected);

expect(source.onInterrupt.emit).not.toHaveBeenCalled();

source.detach();
}));
});
5 changes: 3 additions & 2 deletions projects/core/src/lib/storageinterruptsource.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { WindowInterruptSource } from './windowinterruptsource';
import { EventTargetInterruptOptions } from './eventtargetinterruptsource';

/*
* An interrupt source on the storage event of Window.
*/
export class StorageInterruptSource extends WindowInterruptSource {
constructor(throttleDelay = 500) {
super('storage', throttleDelay);
constructor(options: number | EventTargetInterruptOptions = 500) {
super('storage', options);
}

/*
Expand Down
14 changes: 14 additions & 0 deletions projects/core/src/lib/windowinterruptsource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ describe('core/WindowInterruptSource', () => {

expect(source.onInterrupt.emit).not.toHaveBeenCalled();
}));

it('does not emit onInterrupt event when ssr is true', fakeAsync(() => {
const source = new WindowInterruptSource('focus', { ssr: true });
spyOn(source.onInterrupt, 'emit').and.callThrough();

source.attach();

const expected = new Event('focus');
window.dispatchEvent(expected);

expect(source.onInterrupt.emit).not.toHaveBeenCalled();

source.detach();
}));
});
5 changes: 4 additions & 1 deletion projects/core/src/lib/windowinterruptsource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
*/
export class WindowInterruptSource extends EventTargetInterruptSource {
constructor(events: string, options?: number | EventTargetInterruptOptions) {
super(window, events, options);
const target =
options && (options as EventTargetInterruptOptions).ssr ? null : window;

super(target, events, options);
}
}

0 comments on commit 239be3a

Please sign in to comment.