Skip to content

Commit acf236e

Browse files
authored
Merge pull request #44 from alleyinteractive/issue/43
Corrects the domContentLoaded handler for deferred and async scripts
2 parents c602672 + 2dc2190 commit acf236e

File tree

8 files changed

+120
-29
lines changed

8 files changed

+120
-29
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Change Log
22
This project adheres to [Semantic Versioning](http://semver.org/).
33

4+
## 3.2.0
5+
6+
**Added**
7+
8+
* Setting `config.load` to `true` adds the provider function call in place so it is executed as soon as the parent script is parsed and loaded.
9+
10+
**Fixed**
11+
12+
* The `domContentLoaded` function no longer executes its callback more than once
13+
414
## 3.1.0
515

616
**Added**

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ Component elements are denoted by a `data-component` attribute, the value of whi
4949

5050
**options**: _(Optional)_ - An arbitrary value, typically an object, used by the component. This could be a configuration for another JS library, values used for calculating styles, etc. This is passed to the wrapped function as the `options` property.
5151

52-
**load**: _(Optional)_ - Accepts `false`, array, or a callback. _Default is a `domContentLoaded` callback_.
53-
54-
* `false` will disable loading and instruct `componentProvider` to return the provider function
55-
* An array, in the format of `[HTMLElement, event]`, will listen on `HTMLElement` for `event` (e.g., `[window, 'load']`)
56-
* A callback that accepts a function and contains the logic to call the function
52+
**load**: _(Optional)_ - _Default is a `DOMContentLoaded` handler_.
5753

54+
* `false` - prevents execution and instructs `componentProvider` to return the provider function
55+
* `true` - Adds the provider function call in place so it is executed as soon as the parent script is parsed and loaded.
56+
* `array` - `[HTMLElement, event]` - Adds the provider function as a callback for `event` on `HTMLElement` (e.g., `[window, 'load']`).
57+
* `handler` - A function that accepts a callback and contains the logic to call it.
5858
### Component Properties
5959

6060
Components receive an object of component properties as their only argument. These are based on the config and are included automatically by the framework.
@@ -105,12 +105,13 @@ function productDetails({ element, children, options }) { ... }
105105
const productDetailsConfig = {
106106
name: 'product-details',
107107
component: productDetails,
108-
// load: false | array | function
108+
// load: boolean | array | function
109109
querySelectorAll: {
110110
toggles: '.product-details__toggle',
111111
},
112112
};
113113

114+
// The component is executed at 'DOMContentLoaded' due to the absense of a `config.load` value.
114115
componentProvider(productDetailsConfig);
115116
```
116117

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "js-component-framework",
3-
"version": "3.1.0",
3+
"version": "3.2.0",
44
"description": "A framework for configuring a JavaScript component and attaching it to a DOM element or collection of DOM elements, simplifying organization of DOM interactions on your website.",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/componentLoader.test.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const providerFunc = jest.fn();
1010

1111
test('Does not call the provider function with `load:false`', () => {
1212
componentLoader(providerFunc, false);
13-
expect(providerFunc).toHaveBeenCalledTimes(0);
13+
expect(providerFunc).not.toHaveBeenCalled();
14+
});
15+
16+
test('Does not call the provider function with `load:true`', () => {
17+
componentLoader(providerFunc, true);
18+
expect(providerFunc).not.toHaveBeenCalled();
1419
});
1520

1621
test('Calls the provider function with the loader function', () => {
@@ -22,22 +27,22 @@ test('Calls the provider function with the loader function', () => {
2227

2328
test('Loads on event for single instance', () => {
2429
componentLoader(providerFunc, [eventRoot, 'jscf-test-event']);
25-
expect(providerFunc).toHaveBeenCalledTimes(0);
30+
expect(providerFunc).not.toHaveBeenCalled();
2631

2732
eventRoot.dispatchEvent(new CustomEvent('jscf-test-event'));
2833
expect(providerFunc).toHaveBeenCalledTimes(1);
2934
});
3035

3136
test('Fails silently on invalid event config', () => {
3237
componentLoader(providerFunc, [null, 'jscf-null-test']);
33-
expect(providerFunc).toHaveBeenCalledTimes(0);
38+
expect(providerFunc).not.toHaveBeenCalled();
3439

3540
eventRoot.dispatchEvent(new CustomEvent('jscf-null-test'));
36-
expect(providerFunc).toHaveBeenCalledTimes(0);
41+
expect(providerFunc).not.toHaveBeenCalled();
3742

3843
componentLoader(providerFunc, [eventRoot, 123]);
39-
expect(providerFunc).toHaveBeenCalledTimes(0);
44+
expect(providerFunc).not.toHaveBeenCalled();
4045

4146
eventRoot.dispatchEvent(new CustomEvent('jscf-fail-test'));
42-
expect(providerFunc).toHaveBeenCalledTimes(0);
47+
expect(providerFunc).not.toHaveBeenCalled();
4348
});

src/componentProvider.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,22 @@ export default function componentProvider(config) {
7777
componentArgs.forEach((args) => new Component(args));
7878
};
7979

80-
if (load !== false) {
81-
// Load the provider function.
82-
componentLoader(init, load);
80+
// Return the provider function for later execution.
81+
if (load === false) {
82+
return init;
83+
}
8384

84-
return undefined;
85+
/*
86+
* Call the provider function so it is executed as soon as the document is
87+
* parsed and loaded.
88+
*
89+
* This is a convenience option and is functionally identical to setting
90+
* `config.load` to false and calling the provider function later in the script.
91+
*/
92+
if (load === true) {
93+
return void init(); // eslint-disable-line no-void
8594
}
8695

87-
return init;
96+
// Use the handler defined in the `load` config property.
97+
return void componentLoader(init, load); // eslint-disable-line no-void
8898
}

src/componentProvider.test.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ test('Returns the expected function for single instance when `load:false`', () =
9595
const config = { ...baseConfig, name: 'test-one', load: false };
9696

9797
const providerFunction = componentProvider(config);
98-
expect(config.component).toHaveBeenCalledTimes(0);
98+
expect(config.component).not.toHaveBeenCalled();
9999

100100
providerFunction();
101101
expect(config.component).toHaveBeenCalledTimes(1);
@@ -106,7 +106,8 @@ test('Returns the expected function for multiple instances when `load:false`', (
106106
const config = { ...baseConfig, name: 'test-two', load: false };
107107

108108
const providerFunction = componentProvider(config);
109-
expect(config.component).toHaveBeenCalledTimes(0);
109+
expect(typeof providerFunction).toBe('function');
110+
expect(config.component).not.toHaveBeenCalled();
110111

111112
providerFunction();
112113
expect(config.component).toHaveBeenCalledTimes(2);
@@ -115,6 +116,21 @@ test('Returns the expected function for multiple instances when `load:false`', (
115116
expect(config.component).toHaveBeenNthCalledWith(2, testTwoExpected[1]);
116117
});
117118

119+
test('Calls the component immediately when `load:true`', () => {
120+
const config = { ...baseConfig, name: 'test-one', load: true };
121+
122+
const providerFunction = componentProvider(config);
123+
expect(providerFunction).toBeUndefined();
124+
expect(config.component).toHaveBeenCalledTimes(1);
125+
});
126+
127+
test('Calls the component for multiple instances immediately when `load:true`', () => {
128+
const config = { ...baseConfig, name: 'test-two', load: true };
129+
130+
componentProvider(config);
131+
expect(config.component).toHaveBeenCalledTimes(2);
132+
});
133+
118134
test('Loads on event for single instance', () => {
119135
const config = {
120136
...baseConfig,
@@ -123,7 +139,7 @@ test('Loads on event for single instance', () => {
123139
};
124140

125141
componentProvider(config);
126-
expect(config.component).toHaveBeenCalledTimes(0);
142+
expect(config.component).not.toHaveBeenCalled();
127143

128144
eventRoot.dispatchEvent(new CustomEvent('jscf-test-one-event'));
129145
expect(config.component).toHaveBeenCalledTimes(1);
@@ -138,7 +154,7 @@ test('Loads on event for multiple instances', () => {
138154
};
139155

140156
componentProvider(config);
141-
expect(config.component).toHaveBeenCalledTimes(0);
157+
expect(config.component).not.toHaveBeenCalled();
142158

143159
eventRoot.dispatchEvent(new CustomEvent('jscf-test-two-event'));
144160
expect(config.component).toHaveBeenCalledTimes(2);
@@ -155,10 +171,10 @@ test('Fails silently on `null` event root', () => {
155171
};
156172

157173
componentProvider(config);
158-
expect(config.component).toHaveBeenCalledTimes(0);
174+
expect(config.component).not.toHaveBeenCalled();
159175

160176
eventRoot.dispatchEvent(new CustomEvent('jscf-null-test'));
161-
expect(config.component).toHaveBeenCalledTimes(0);
177+
expect(config.component).not.toHaveBeenCalled();
162178
});
163179

164180
test('Provides an empty options object when options are undefined', () => {

src/domContentLoaded.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
/* eslint consistent-return: ["error", { "treatUndefinedAsUnspecified": true }] */
2+
13
/**
24
* Executes the given callback when DOMContentLoaded is ready.
35
*
4-
* @param {function} cb Callback to execute once DOMContentLoaded completes.
6+
* @param {function} callback Callback to execute once DOMContentLoaded completes.
57
*/
6-
const domContentLoaded = (cb) => {
8+
function domContentLoaded(callback) {
79
if (
810
document.readyState === 'complete'
911
|| document.readyState === 'interactive'
1012
) {
11-
cb();
13+
return void callback(); // eslint-disable-line no-void
1214
}
1315

14-
document.addEventListener('DOMContentLoaded', cb, { once: true });
15-
};
16+
document.addEventListener('DOMContentLoaded', callback, { once: true });
17+
}
1618

1719
export default domContentLoaded;

src/domContentLoaded.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import domContentLoaded from './domContentLoaded';
2+
3+
const providerFunc = jest.fn();
4+
const addEventListener = jest.fn(() => {});
5+
6+
beforeAll(() => {
7+
// Allows setting document.readyState.
8+
Object.defineProperty(document, 'readyState', {
9+
value: 'loading',
10+
writable: true,
11+
});
12+
13+
Object.defineProperty(document, 'addEventListener', {
14+
value: addEventListener,
15+
});
16+
});
17+
18+
afterEach(() => jest.clearAllMocks());
19+
20+
test("'loading' readyState uses DOMContentLoaded", () => {
21+
domContentLoaded(providerFunc);
22+
expect(providerFunc).not.toHaveBeenCalled();
23+
24+
expect(addEventListener).toHaveBeenCalledWith(
25+
'DOMContentLoaded',
26+
providerFunc,
27+
{ once: true },
28+
);
29+
});
30+
31+
test("'complete' readyState calls the function immediately", () => {
32+
document.readyState = 'complete';
33+
34+
domContentLoaded(providerFunc);
35+
expect(providerFunc).toHaveBeenCalledTimes(1);
36+
37+
expect(addEventListener).not.toHaveBeenCalled();
38+
});
39+
40+
test("'interactive' readyState calls the funciton immediately", () => {
41+
document.readyState = 'interactive';
42+
43+
domContentLoaded(providerFunc);
44+
expect(providerFunc).toHaveBeenCalledTimes(1);
45+
46+
expect(addEventListener).not.toHaveBeenCalled();
47+
});

0 commit comments

Comments
 (0)