From 79b080671843e18eb30163594877b5a27b4768bf Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 1 Apr 2020 20:53:59 +0300 Subject: [PATCH] feat(core): add loading strategies --- .../packages/core/src/lib/strategies/index.ts | 1 + .../src/lib/strategies/loading.strategy.ts | 68 ++++++++++++++ .../src/lib/tests/loading.strategy.spec.ts | 90 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/strategies/index.ts b/npm/ng-packs/packages/core/src/lib/strategies/index.ts index d1a367e138..915887f92a 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/index.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/index.ts @@ -1,2 +1,3 @@ export * from './cross-origin.strategy'; export * from './dom.strategy'; +export * from './loading.strategy'; diff --git a/npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts new file mode 100644 index 0000000000..eae23a0c7f --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts @@ -0,0 +1,68 @@ +import { Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { fromLazyLoad } from '../utils'; +import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy'; +import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; + +export abstract class LoadingStrategy { + constructor( + public path: string, + protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), + protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(), + ) {} + + abstract createElement(): T; + + createStream(): Observable { + return of(null).pipe( + switchMap(() => + fromLazyLoad(this.createElement(), this.domStrategy, this.crossOriginStrategy), + ), + ); + } +} + +export class ScriptLoadingStrategy extends LoadingStrategy { + constructor(src: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) { + super(src, domStrategy, crossOriginStrategy); + } + + createElement(): HTMLScriptElement { + const element = document.createElement('script'); + element.src = this.path; + + return element; + } +} + +export class StyleLoadingStrategy extends LoadingStrategy { + constructor(href: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) { + super(href, domStrategy, crossOriginStrategy); + } + + createElement(): HTMLLinkElement { + const element = document.createElement('link'); + element.rel = 'stylesheet'; + element.href = this.path; + + return element; + } +} + +export const LOADING_STRATEGY = { + AppendAnonymousScriptToBody(src: string) { + return new ScriptLoadingStrategy(src, DOM_STRATEGY.AppendToBody()); + }, + AppendAnonymousScriptToHead(src: string) { + return new ScriptLoadingStrategy(src); + }, + AppendAnonymousStyleToHead(src: string) { + return new StyleLoadingStrategy(src); + }, + PrependAnonymousScriptToHead(src: string) { + return new ScriptLoadingStrategy(src, DOM_STRATEGY.PrependToHead()); + }, + PrependAnonymousStyleToHead(src: string) { + return new StyleLoadingStrategy(src, DOM_STRATEGY.PrependToHead()); + }, +}; diff --git a/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts new file mode 100644 index 0000000000..9260cd8948 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts @@ -0,0 +1,90 @@ +import { + CROSS_ORIGIN_STRATEGY, + DOM_STRATEGY, + LOADING_STRATEGY, + ScriptLoadingStrategy, + StyleLoadingStrategy, +} from '../strategies'; + +const path = 'http://example.com/'; + +describe('ScriptLoadingStrategy', () => { + describe('#createElement', () => { + it('should return a script element with src attribute', () => { + const strategy = new ScriptLoadingStrategy(path); + const element = strategy.createElement(); + + expect(element.tagName).toBe('SCRIPT'); + expect(element.src).toBe(path); + }); + }); + + describe('#createStream', () => { + it('should use given dom and cross-origin strategies', done => { + const domStrategy = DOM_STRATEGY.PrependToHead(); + const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); + + domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => { + setTimeout(() => { + el.onload(new CustomEvent('success', { detail: el.crossOrigin })); + }, 0); + }) as any; + + const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy); + + strategy.createStream().subscribe(event => { + expect(event.detail).toBe('use-credentials'); + done(); + }); + }); + }); +}); + +describe('StyleLoadingStrategy', () => { + describe('#createElement', () => { + it('should return a style element with href and rel attributes', () => { + const strategy = new StyleLoadingStrategy(path); + const element = strategy.createElement(); + + expect(element.tagName).toBe('LINK'); + expect(element.href).toBe(path); + expect(element.rel).toBe('stylesheet'); + }); + }); + + describe('#createStream', () => { + it('should use given dom and cross-origin strategies', done => { + const domStrategy = DOM_STRATEGY.PrependToHead(); + const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); + + domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => { + setTimeout(() => { + el.onload(new CustomEvent('success', { detail: el.crossOrigin })); + }, 0); + }) as any; + + const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy); + + strategy.createStream().subscribe(event => { + expect(event.detail).toBe('use-credentials'); + done(); + }); + }); + }); +}); + +describe('LOADING_STRATEGY', () => { + test.each` + name | Strategy | domStrategy + ${'AppendAnonymousScriptToBody'} | ${ScriptLoadingStrategy} | ${'AppendToBody'} + ${'AppendAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'AppendToHead'} + ${'AppendAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'AppendToHead'} + ${'PrependAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'PrependToHead'} + ${'PrependAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'PrependToHead'} + `( + 'should successfully map $name to $Strategy.name with $domStrategy dom strategy', + ({ name, Strategy, domStrategy }) => { + expect(LOADING_STRATEGY[name](path)).toEqual(new Strategy(path, DOM_STRATEGY[domStrategy]())); + }, + ); +});