Skip to content

Commit 06fd92b

Browse files
committed
feat(common): CHECKOUT-4400 Add StylesheetLoader for loading stylesheets
1 parent c3a3a98 commit 06fd92b

File tree

6 files changed

+158
-0
lines changed

6 files changed

+158
-0
lines changed

src/create-stylesheet-loader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import StylesheetLoader from './stylesheet-loader';
2+
3+
export default function createStylesheetLoader(): StylesheetLoader {
4+
return new StylesheetLoader();
5+
}

src/get-stylesheet-loader.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import getStylesheetLoader from './get-stylesheet-loader';
2+
import StylesheetLoader from './stylesheet-loader';
3+
4+
describe('getStylesheetLoader()', () => {
5+
it('returns same `StylesheetLoader` instance', () => {
6+
const loader = getStylesheetLoader();
7+
const loader2 = getStylesheetLoader();
8+
9+
expect(loader)
10+
.toBe(loader2);
11+
12+
expect(loader)
13+
.toBeInstanceOf(StylesheetLoader);
14+
});
15+
});

src/get-stylesheet-loader.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import createStylesheetLoader from './create-stylesheet-loader';
2+
import StylesheetLoader from './stylesheet-loader';
3+
4+
let instance: StylesheetLoader;
5+
6+
export default function getStylesheetLoader(): StylesheetLoader {
7+
if (!instance) {
8+
instance = createStylesheetLoader();
9+
}
10+
11+
return instance;
12+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export { default as ScriptLoader } from './script-loader';
22
export { default as createScriptLoader } from './create-script-loader';
33
export { default as getScriptLoader } from './get-script-loader';
4+
5+
export { default as StylesheetLoader } from './stylesheet-loader';
6+
export { default as createStylesheetLoader } from './create-stylesheet-loader';
7+
export { default as getStylesheetLoader } from './get-stylesheet-loader';

src/stylesheet-loader.spec.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import StylesheetLoader from './stylesheet-loader';
2+
3+
describe('StylesheetLoader', () => {
4+
let loader: StylesheetLoader;
5+
let stylesheet: HTMLLinkElement;
6+
7+
beforeEach(() => {
8+
stylesheet = document.createElement('link');
9+
10+
jest.spyOn(document, 'createElement')
11+
.mockImplementation(() => stylesheet);
12+
13+
loader = new StylesheetLoader();
14+
});
15+
16+
afterEach(() => {
17+
jest.restoreAllMocks();
18+
});
19+
20+
describe('when stylesheet loads successfully', () => {
21+
beforeEach(() => {
22+
jest.spyOn(document.head, 'appendChild')
23+
.mockImplementation(element =>
24+
setTimeout(() => element.onload(new Event('load')), 0)
25+
);
26+
});
27+
28+
it('attaches link tag to document', async () => {
29+
await loader.loadStylesheet('https://foo.bar/hello-world.css');
30+
31+
expect(document.head.appendChild)
32+
.toHaveBeenCalledWith(stylesheet);
33+
34+
expect(stylesheet.href)
35+
.toEqual('https://foo.bar/hello-world.css');
36+
});
37+
38+
it('resolves promise if stylesheet is loaded', async () => {
39+
const output = await loader.loadStylesheet('https://foo.bar/hello-world.css');
40+
41+
expect(output)
42+
.toBeInstanceOf(Event);
43+
});
44+
45+
it('does not load same stylesheet twice', async () => {
46+
await loader.loadStylesheet('https://foo.bar/hello-world.css');
47+
await loader.loadStylesheet('https://foo.bar/hello-world.css');
48+
49+
expect(document.head.appendChild)
50+
.toHaveBeenCalledTimes(1);
51+
});
52+
});
53+
54+
describe('when stylesheet fails to load', () => {
55+
beforeEach(() => {
56+
jest.spyOn(document.head, 'appendChild')
57+
.mockImplementation(element =>
58+
setTimeout(() => element.onerror(new Event('error')), 0)
59+
);
60+
});
61+
62+
it('rejects promise if stylesheet is not loaded', async () => {
63+
await loader.loadStylesheet('https://foo.bar/hello-world.css')
64+
.catch(error =>
65+
expect(error)
66+
.toBeTruthy()
67+
);
68+
});
69+
70+
it('loads the script again', async () => {
71+
const errorHandler = jest.fn();
72+
73+
await loader.loadStylesheet('https://foo.bar/hello-world.css')
74+
.catch(errorHandler);
75+
76+
await loader.loadStylesheet('https://foo.bar/hello-world.css')
77+
.catch(errorHandler);
78+
79+
expect(document.head.appendChild)
80+
.toHaveBeenCalledTimes(2);
81+
82+
expect(errorHandler)
83+
.toHaveBeenCalledTimes(2);
84+
});
85+
});
86+
});

src/stylesheet-loader.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export interface LoadStylesheetOptions {
2+
prepend: boolean;
3+
}
4+
5+
export default class StylesheetLoader {
6+
private _stylesheets: { [key: string]: Promise<Event> } = {};
7+
8+
loadStylesheet(src: string, options?: LoadStylesheetOptions): Promise<Event> {
9+
if (!this._stylesheets[src]) {
10+
this._stylesheets[src] = new Promise((resolve, reject) => {
11+
const stylesheet = document.createElement('link');
12+
const { prepend = false } = options || {};
13+
14+
stylesheet.onload = event => resolve(event);
15+
stylesheet.onerror = event => {
16+
delete this._stylesheets[src];
17+
reject(event);
18+
};
19+
stylesheet.rel = 'stylesheet';
20+
stylesheet.href = src;
21+
22+
if (prepend && document.head.children[0]) {
23+
document.head.insertBefore(stylesheet, document.head.children[0]);
24+
} else {
25+
document.head.appendChild(stylesheet);
26+
}
27+
});
28+
}
29+
30+
return this._stylesheets[src];
31+
}
32+
33+
loadStylesheets(urls: string[], options?: LoadStylesheetOptions): Promise<Event[]> {
34+
return Promise.all(urls.map(url => this.loadStylesheet(url, options)));
35+
}
36+
}

0 commit comments

Comments
 (0)