Browse Source

Merge pull request #3453 from abpframework/feat/3185

Introduced New LazyLoadService
pull/3463/head
Mehmet Erim 6 years ago
committed by GitHub
parent
commit
22f1d588ea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      docs/en/UI/Angular/Content-Security-Strategy.md
  2. 41
      docs/en/UI/Angular/Cross-Origin-Strategy.md
  3. 2
      docs/en/UI/Angular/Custom-Setting-Page.md
  4. 56
      docs/en/UI/Angular/Dom-Strategy.md
  5. 136
      docs/en/UI/Angular/Lazy-Load-Service.md
  6. 75
      docs/en/UI/Angular/Loading-Strategy.md
  7. 4
      docs/en/docs-nav.json
  8. 42
      npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts
  9. 32
      npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts
  10. 17
      npm/ng-packs/packages/core/src/lib/strategies/cross-origin.strategy.ts
  11. 28
      npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts
  12. 4
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  13. 88
      npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts
  14. 41
      npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts
  15. 38
      npm/ng-packs/packages/core/src/lib/tests/cross-origin.strategy.spec.ts
  16. 49
      npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts
  17. 113
      npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts
  18. 74
      npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts
  19. 102
      npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts
  20. 1
      npm/ng-packs/packages/core/src/lib/utils/index.ts
  21. 51
      npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts
  22. 4
      npm/ng-packs/packages/core/src/public-api.ts

56
docs/en/UI/Angular/Content-Security-Strategy.md

@ -0,0 +1,56 @@
# ContentSecurityStrategy
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
## API
### constructor(public nonce?: string)
`nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives.
### applyCSP(element: HTMLScriptElement | HTMLStyleElement): void
This method maps the aforementioned properties to the given `element`.
## LooseContentSecurityPolicy
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It required `nonce` and marks given `<script>` or `<style>` tag with it.
## StrictContentSecurityPolicy
`StrictContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative.
## Predefined Content Security Strategies
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant.
### Loose(nonce: string)
`nonce` will be set.
### Strict()
Nothing will be done.
## What's Next?
TODO: Place new InsertionStrategy link here.

41
docs/en/UI/Angular/Cross-Origin-Strategy.md

@ -0,0 +1,41 @@
# CrossOriginStrategy
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string)
`crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin).
`integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity).
### setCrossOrigin(element: HTMLElement): void
This method maps the aforementioned properties to the given `element`.
## Predefined Cross-Origin Strategies
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant.
### Anonymous(integrity?: string)
`crossorigin` will be set as `"anonymous"` and `integrity` is optional.
### UseCredentials(integrity?: string)
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)

2
docs/en/UI/Angular/Custom-Setting-Page.md

@ -43,4 +43,4 @@ Navigate to `/setting-management` route to see the changes:
## What's Next?
- [TrackByService](./Track-By-Service.md)
- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md)

56
docs/en/UI/Angular/Dom-Strategy.md

@ -0,0 +1,56 @@
# DomStrategy
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor(public target?: HTMLElement, public position?: InsertPosition)
`target` is an HTMLElement (_default: document.head_).
`position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_).
### insertElement(element: HTMLElement): void
This method inserts given `element` to `target` based on the `position`.
## Predefined Dom Strategies
Predefined dom strategies are accessible via `DOM_STRATEGY` constant.
### AppendToBody()
`insertElement` will place the given `element` at the end of `<body>`.
### AppendToHead()
`insertElement` will place the given `element` at the end of `<head>`.
### PrependToHead()
`insertElement` will place the given `element` at the beginning of `<head>`.
### AfterElement(target: HTMLElement)
`insertElement` will place the given `element` after (as a sibling to) the `target`.
### BeforeElement(target: HTMLElement)
`insertElement` will place the given `element` before (as a sibling to) the `target`.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)

136
docs/en/UI/Angular/Lazy-Load-Service.md

@ -0,0 +1,136 @@
# How to Lazy Load Scripts and Styles
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy loading scripts and styles in an easy and explicit way.
## Getting Started
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { LazyLoadService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
}
```
## Usage
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<style>` element in the DOM at the desired position and force the browser to download the target resource.
### How to Load Scripts
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="libraryLoaded$ | async"></some-component>
`
})
class DemoComponent {
libraryLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### How to Load Styles
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="stylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
stylesLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### Advanced Usage
You have quite a bit of freedom to define how your lazy load will be implemented. Here is an example:
```js
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh',
);
const loadingStrategy = new StyleLoadingStrategy(
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css',
domStrategy,
crossOriginStrategy,
);
this.lazyLoad.load(loadingStrategy, 1, 2000);
```
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails.
## API
### loaded: Set<string>
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
### load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>
`strategy` parameter is the primary focus here and is explained above.
`retryTimes` defines how many times the loading will be tried again before fail (_default: 2_).
`retryDelay` defines how much delay there will be between retries (_default: 1000_).
## What's Next?
- [TrackByService](./Track-By-Service.md)

75
docs/en/UI/Angular/Loading-Strategy.md

@ -0,0 +1,75 @@
# LoadingStrategy
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. Its instances help you mark inline script or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
## API
### constructor(public path: string, protected domStrategy?: DomStrategy, protected crossOriginStrategy?: CrossOriginStrategy)
`path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute.
`domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
`crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_)
### createElement(): HTMLScriptElement | HTMLLinkElement
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`.
### createStream(): Observable<Event>
This method creates and returns an observable stream that emits on success and throws on error.
## ScriptLoadingStrategy
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you lazy load a script.
## StyleLoadingStrategy
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you lazy load a style.
## Predefined Loading Strategies
Predefined content security strategies are accessible via `LOADING_STRATEGY` constant.
### AppendAnonymousScriptToHead(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousScriptToHead(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document.
### AppendAnonymousScriptToBody(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document.
### AppendAnonymousStyleToHead(href: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousStyleToHead(href: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document.
## What's Next?
- [LazyLoadService](./Lazy-Load-Service.md)

4
docs/en/docs-nav.json

@ -341,6 +341,10 @@
"text": "Custom Setting Page",
"path": "UI/Angular/Custom-Setting-Page.md"
},
{
"text": "Lazy Loading Scripts & Styles",
"path": "UI/Angular/Lazy-Load-Service.md"
},
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"

42
npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts

@ -1,20 +1,56 @@
import { Injectable } from '@angular/core';
import { Observable, ReplaySubject, throwError } from 'rxjs';
import { concat, Observable, of, ReplaySubject, throwError } from 'rxjs';
import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators';
import { LoadingStrategy } from '../strategies';
import { uuid } from '../utils';
@Injectable({
providedIn: 'root',
})
export class LazyLoadService {
readonly loaded = new Set();
loadedLibraries: { [url: string]: ReplaySubject<void> } = {};
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>;
load(
urlOrUrls: string | string[],
type: 'script' | 'style',
content: string = '',
content?: string,
targetQuery?: string,
position?: InsertPosition,
): Observable<void>;
load(
strategyOrUrl: LoadingStrategy | string | string[],
retryTimesOrType?: number | 'script' | 'style',
retryDelayOrContent?: number | string,
targetQuery: string = 'body',
position: InsertPosition = 'beforeend',
): Observable<void> {
): Observable<Event | void> {
if (strategyOrUrl instanceof LoadingStrategy) {
const strategy = strategyOrUrl;
const retryTimes = typeof retryTimesOrType === 'number' ? retryTimesOrType : 2;
const retryDelay = typeof retryDelayOrContent === 'number' ? retryDelayOrContent : 1000;
if (this.loaded.has(strategy.path)) return of(new CustomEvent('load'));
return strategy.createStream().pipe(
retryWhen(error$ =>
concat(
error$.pipe(delay(retryDelay), take(retryTimes)),
throwError(new CustomEvent('error')),
),
),
tap(() => this.loaded.add(strategy.path)),
delay(100),
shareReplay({ bufferSize: 1, refCount: true }),
);
}
let urlOrUrls = strategyOrUrl;
const content = (retryDelayOrContent as string) || '';
const type = retryTimesOrType as 'script' | 'style';
if (!urlOrUrls && !content) {
return throwError('Should pass url or content');
} else if (!urlOrUrls && content) {

32
npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts

@ -0,0 +1,32 @@
export abstract class ContentSecurityStrategy {
constructor(public nonce?: string) {}
abstract applyCSP(element: HTMLScriptElement | HTMLStyleElement): void;
}
export class LooseContentSecurityStrategy extends ContentSecurityStrategy {
constructor(nonce: string) {
super(nonce);
}
applyCSP(element: HTMLScriptElement | HTMLStyleElement) {
element.setAttribute('nonce', this.nonce);
}
}
export class StrictContentSecurityStrategy extends ContentSecurityStrategy {
constructor() {
super();
}
applyCSP(_: HTMLScriptElement | HTMLStyleElement) {}
}
export const CONTENT_SECURITY_STRATEGY = {
Loose(nonce: string) {
return new LooseContentSecurityStrategy(nonce);
},
Strict() {
return new StrictContentSecurityStrategy();
},
};

17
npm/ng-packs/packages/core/src/lib/strategies/cross-origin.strategy.ts

@ -0,0 +1,17 @@
export class CrossOriginStrategy {
constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string) {}
setCrossOrigin<T extends HTMLElement>(element: T) {
if (this.integrity) element.setAttribute('integrity', this.integrity);
element.setAttribute('crossorigin', this.crossorigin);
}
}
export const CROSS_ORIGIN_STRATEGY = {
Anonymous(integrity?: string) {
return new CrossOriginStrategy('anonymous', integrity);
},
UseCredentials(integrity?: string) {
return new CrossOriginStrategy('use-credentials', integrity);
},
};

28
npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts

@ -0,0 +1,28 @@
export class DomStrategy {
constructor(
public target: HTMLElement = document.head,
public position: InsertPosition = 'beforeend',
) {}
insertElement<T extends HTMLElement>(element: T) {
this.target.insertAdjacentElement(this.position, element);
}
}
export const DOM_STRATEGY = {
AfterElement(element: HTMLElement) {
return new DomStrategy(element, 'afterend');
},
AppendToBody() {
return new DomStrategy(document.body, 'beforeend');
},
AppendToHead() {
return new DomStrategy(document.head, 'beforeend');
},
BeforeElement(element: HTMLElement) {
return new DomStrategy(element, 'beforebegin');
},
PrependToHead() {
return new DomStrategy(document.head, 'afterbegin');
},
};

4
npm/ng-packs/packages/core/src/lib/strategies/index.ts

@ -0,0 +1,4 @@
export * from './content-security.strategy';
export * from './cross-origin.strategy';
export * from './dom.strategy';
export * from './loading.strategy';

88
npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts

@ -0,0 +1,88 @@
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<T extends HTMLScriptElement | HTMLLinkElement = any> {
constructor(
public path: string,
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
) {}
abstract createElement(): T;
createStream<E extends Event>(): Observable<E> {
return of(null).pipe(
switchMap(() =>
fromLazyLoad<E>(this.createElement(), this.domStrategy, this.crossOriginStrategy),
),
);
}
}
export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
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<HTMLLinkElement> {
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, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.AppendToBody(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
AppendAnonymousScriptToHead(src: string, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.AppendToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
AppendAnonymousStyleToHead(src: string, integrity?: string) {
return new StyleLoadingStrategy(
src,
DOM_STRATEGY.AppendToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
PrependAnonymousScriptToHead(src: string, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.PrependToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
PrependAnonymousStyleToHead(src: string, integrity?: string) {
return new StyleLoadingStrategy(
src,
DOM_STRATEGY.PrependToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
};

41
npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts

@ -0,0 +1,41 @@
import {
CONTENT_SECURITY_STRATEGY,
LooseContentSecurityStrategy,
StrictContentSecurityStrategy,
} from '../strategies';
import { uuid } from '../utils';
describe('LooseContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should set nonce attribute', () => {
const nonce = uuid();
const strategy = new LooseContentSecurityStrategy(nonce);
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBe(nonce);
});
});
});
describe('StrictContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should not set nonce attribute', () => {
const strategy = new StrictContentSecurityStrategy();
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBeNull();
});
});
});
describe('CONTENT_SECURITY_STRATEGY', () => {
test.each`
name | Strategy | nonce
${'Loose'} | ${LooseContentSecurityStrategy} | ${uuid()}
${'Strict'} | ${StrictContentSecurityStrategy} | ${undefined}
`('should successfully map $name to $Strategy.name', ({ name, Strategy, nonce }) => {
expect(CONTENT_SECURITY_STRATEGY[name](nonce)).toEqual(new Strategy(nonce));
});
});

38
npm/ng-packs/packages/core/src/lib/tests/cross-origin.strategy.spec.ts

@ -0,0 +1,38 @@
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies';
import { uuid } from '../utils';
describe('CrossOriginStrategy', () => {
describe('#setCrossOrigin', () => {
it('should set crossorigin attribute', () => {
const strategy = new CrossOriginStrategy('use-credentials');
const element = document.createElement('link');
strategy.setCrossOrigin(element);
expect(element.crossOrigin).toBe('use-credentials');
});
it('should set integrity attribute when given', () => {
const integrity = uuid();
const strategy = new CrossOriginStrategy('anonymous', integrity);
const element = document.createElement('link');
strategy.setCrossOrigin(element);
expect(element.crossOrigin).toBe('anonymous');
expect(element.getAttribute('integrity')).toBe(integrity);
});
});
});
describe('CROSS_ORIGIN_STRATEGY', () => {
test.each`
name | integrity | crossOrigin
${'Anonymous'} | ${undefined} | ${'anonymous'}
${'Anonymous'} | ${uuid()} | ${'anonymous'}
${'UseCredentials'} | ${undefined} | ${'use-credentials'}
${'UseCredentials'} | ${uuid()} | ${'use-credentials'}
`('should successfully map $name to CrossOriginStrategy', ({ name, integrity, crossOrigin }) => {
expect(CROSS_ORIGIN_STRATEGY[name](integrity)).toEqual(
new CrossOriginStrategy(crossOrigin, integrity),
);
});
});

49
npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts

@ -0,0 +1,49 @@
import { DomStrategy, DOM_STRATEGY } from '../strategies';
describe('DomStrategy', () => {
describe('#insertElement', () => {
it('should append element to head by default', () => {
const strategy = new DomStrategy();
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.head.lastChild).toBe(element);
});
it('should append element to body when body is given as target', () => {
const strategy = new DomStrategy(document.body);
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.body.lastChild).toBe(element);
});
it('should prepend to head when position is given as "afterbegin"', () => {
const strategy = new DomStrategy(undefined, 'afterbegin');
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.head.firstChild).toBe(element);
});
});
});
describe('DOM_STRATEGY', () => {
const div = document.createElement('DIV');
beforeEach(() => {
document.body.innerHTML = '';
document.body.appendChild(div);
});
test.each`
name | target | position
${'AfterElement'} | ${div} | ${'afterend'}
${'AppendToBody'} | ${document.body} | ${'beforeend'}
${'AppendToHead'} | ${document.head} | ${'beforeend'}
${'BeforeElement'} | ${div} | ${'beforebegin'}
${'PrependToHead'} | ${document.head} | ${'afterbegin'}
`('should successfully map $name to CrossOriginStrategy', ({ name, target, position }) => {
expect(DOM_STRATEGY[name](target)).toEqual(new DomStrategy(target, position));
});
});

113
npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts

@ -0,0 +1,113 @@
import { DomStrategy, DOM_STRATEGY } from '../strategies';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { uuid } from '../utils';
import { fromLazyLoad } from '../utils/lazy-load-utils';
describe('Lazy Load Utils', () => {
describe('#fromLazyLoad', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should append to head by default', () => {
const element = document.createElement('link');
const spy = jest.spyOn(document.head, 'insertAdjacentElement');
fromLazyLoad(element);
expect(spy).toHaveBeenCalledWith('beforeend', element);
});
it('should allow setting a dom strategy', () => {
const element = document.createElement('link');
const spy = jest.spyOn(document.head, 'insertAdjacentElement');
fromLazyLoad(element, DOM_STRATEGY.PrependToHead());
expect(spy).toHaveBeenCalledWith('afterbegin', element);
});
it('should set crossorigin to "anonymous" by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.crossOrigin).toBe('anonymous');
});
it('should not set integrity by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.getAttribute('integrity')).toBeNull();
});
it('should allow setting a cross-origin strategy', () => {
const element = document.createElement('link');
const integrity = uuid();
fromLazyLoad(element, undefined, CROSS_ORIGIN_STRATEGY.UseCredentials(integrity));
expect(element.crossOrigin).toBe('use-credentials');
expect(element.getAttribute('integrity')).toBe(integrity);
});
it('should emit error event on fail and clear callbacks', done => {
const error = new CustomEvent('error');
const parentNode = { removeChild: jest.fn() };
const element = ({ parentNode } as any) as HTMLLinkElement;
fromLazyLoad(
element,
{
insertElement(el: HTMLLinkElement) {
expect(el).toBe(element);
setTimeout(() => {
el.onerror(error);
}, 0);
},
} as DomStrategy,
{
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
).subscribe({
error: value => {
expect(value).toBe(error);
expect(parentNode.removeChild).toHaveBeenCalledWith(element);
expect(element.onerror).toBeNull();
done();
},
});
});
it('should emit load event on success and clear callbacks', done => {
const success = new CustomEvent('load');
const parentNode = { removeChild: jest.fn() };
const element = ({ parentNode } as any) as HTMLLinkElement;
fromLazyLoad(
element,
{
insertElement(el: HTMLLinkElement) {
expect(el).toBe(element);
setTimeout(() => {
el.onload(success);
}, 0);
},
} as DomStrategy,
{
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
).subscribe({
next: value => {
expect(value).toBe(success);
expect(parentNode.removeChild).not.toHaveBeenCalled();
expect(element.onload).toBeNull();
done();
},
});
});
});
});

74
npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts

@ -1,9 +1,65 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { LazyLoadService } from '../services/lazy-load.service';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ScriptLoadingStrategy } from '../strategies';
describe('LazyLoadService', () => {
describe('#load', () => {
const service = new LazyLoadService();
const strategy = new ScriptLoadingStrategy('http://example.com/');
afterEach(() => {
jest.clearAllMocks();
});
it('should emit an error event if not loaded', done => {
const counter = jest.fn();
jest.spyOn(strategy, 'createStream').mockReturnValueOnce(
of(null).pipe(
switchMap(() => {
counter();
return throwError('THIS WILL NOT BE THE FINAL ERROR');
}),
),
);
service.load(strategy, 5, 0).subscribe({
error: errorEvent => {
expect(errorEvent).toEqual(new CustomEvent('error'));
expect(counter).toHaveBeenCalledTimes(6);
expect(service.loaded.has(strategy.path)).toBe(false);
done();
},
});
});
it('should emit a load event if loaded', done => {
const loadEvent = new CustomEvent('load');
jest.spyOn(strategy, 'createStream').mockReturnValue(of(loadEvent));
service.load(strategy).subscribe({
next: event => {
expect(event).toBe(loadEvent);
expect(service.loaded.has(strategy.path)).toBe(true);
done();
},
});
});
it('should emit a custom load event if loaded if resource is loaded before', done => {
const loadEvent = new CustomEvent('load');
service.loaded.add(strategy.path);
service.load(strategy).subscribe(event => {
expect(event).toEqual(loadEvent);
done();
});
});
});
});
describe('LazyLoadService (Deprecated)', () => {
let spectator: SpectatorService<LazyLoadService>;
let service: LazyLoadService;
const scriptElement = document.createElement('script');
@ -25,15 +81,17 @@ describe('LazyLoadService', () => {
spy.mockReturnValue(scriptElement);
service.load('https://abp.io', 'script', 'test').subscribe(res => {
expect(document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent).toMatch(
'test',
);
expect(
document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent,
).toMatch('test');
});
scriptElement.onload(null);
service.load('https://abp.io', 'script', 'test').subscribe(res => {
expect(document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]')).toHaveLength(1);
expect(
document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]'),
).toHaveLength(1);
done();
});
});
@ -59,7 +117,9 @@ describe('LazyLoadService', () => {
test('should load an link element', done => {
service.load('https://abp.io', 'style').subscribe(res => {
expect(document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]')).toBeTruthy();
expect(
document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]'),
).toBeTruthy();
done();
});

102
npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts

@ -0,0 +1,102 @@
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: {
crossOrigin: el.crossOrigin,
},
}),
);
}, 0);
}) as any;
const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).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: {
crossOrigin: el.crossOrigin,
},
}),
);
}, 0);
}) as any;
const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).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]()));
},
);
});

1
npm/ng-packs/packages/core/src/lib/utils/index.ts

@ -1,5 +1,6 @@
export * from './common-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './lazy-load-utils';
export * from './route-utils';
export * from './rxjs-utils';

51
npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts

@ -0,0 +1,51 @@
import { Observable, Observer } from 'rxjs';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from '../strategies/dom.strategy';
export function fromLazyLoad<T extends Event>(
element: HTMLScriptElement | HTMLLinkElement,
domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
): Observable<T> {
crossOriginStrategy.setCrossOrigin(element);
domStrategy.insertElement(element);
return new Observable((observer: Observer<T>) => {
element.onload = (event: T) => {
clearCallbacks(element);
observer.next(event);
observer.complete();
};
const handleError = createErrorHandler(observer, element);
element.onerror = handleError;
element.onabort = handleError;
element.onemptied = handleError;
element.onstalled = handleError;
element.onsuspend = handleError;
return () => {
clearCallbacks(element);
observer.complete();
};
});
}
function createErrorHandler(observer: Observer<Event>, element: HTMLElement) {
/* tslint:disable-next-line:only-arrow-functions */
return function(event: Event | string) {
clearCallbacks(element);
element.parentNode.removeChild(element);
observer.error(event);
};
}
function clearCallbacks(element: HTMLElement) {
element.onload = null;
element.onerror = null;
element.onabort = null;
element.onemptied = null;
element.onstalled = null;
element.onsuspend = null;
}

4
npm/ng-packs/packages/core/src/public-api.ts

@ -7,6 +7,7 @@ export * from './lib/abstracts';
export * from './lib/actions';
export * from './lib/components';
export * from './lib/constants';
export * from './lib/core.module';
export * from './lib/directives';
export * from './lib/enums';
export * from './lib/guards';
@ -16,7 +17,6 @@ export * from './lib/pipes';
export * from './lib/plugins';
export * from './lib/services';
export * from './lib/states';
export * from './lib/strategies';
export * from './lib/tokens';
export * from './lib/utils';
export * from './lib/core.module';

Loading…
Cancel
Save