Skip to content

Commit 191e525

Browse files
authored
Merge pull request dozoisch#30 from dozoisch/dynamic_url
Dynamic URL building
2 parents 182f6da + d86c42c commit 191e525

File tree

7 files changed

+94
-50
lines changed

7 files changed

+94
-50
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"no-multiple-empty-lines": [1, { "max": 1}],
2020
"no-underscore-dangle": 0,
2121
"no-unused-vars": [1, { "vars": "all", "args": "none" }],
22-
"no-undef": 1,
22+
"no-undef": 2,
2323
"no-var": 2,
2424
"quote-props": [2, "as-needed"],
2525
"quotes": [2, "double"],

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ build/Release
2727
node_modules
2828

2929
/lib/
30+
31+
32+
# Editors
33+
.vscode

.npmignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ tools/
44
.gitignore
55
.travis.yml
66
karma.conf.js
7-
.babelrc
7+
.babelrc
8+
.vscode

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ I decided push react-script-loader a bit further and make a composition function
2020

2121
## Usage
2222

23-
The api is very simple `makeAsyncScriptLoader(Component, scriptUrl, options)`. Where options can contain exposeFuncs, callbackName and globalName.
23+
The api is very simple `makeAsyncScriptLoader(Component, getScriptUrl, options)`. Where options can contain exposeFuncs, callbackName and globalName.
2424

2525
- Component: The component to wrap.
26-
- scriptUrl: the full of the script tag.
26+
- getScriptUrl: a string or function that returns the full URL of the script tag.
2727
- options *(optional)*:
2828
- exposeFuncs: Array of String. It'll create a function that will call the child component with the same name. It passes arguments and return value.
2929
- callbackName: If the scripts calls a global function when loaded, provide the callback name here. It'll be autoregistered on the window.

src/async-script-loader.js

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ let SCRIPT_MAP = {};
66
// A counter used to generate a unique id for each component that uses the function
77
let idCount = 0;
88

9-
export default function makeAsyncScript(Component, scriptURL, options) {
9+
export default function makeAsyncScript(Component, getScriptURL, options) {
1010
options = options || {};
11-
const wrappedComponentName = Component.displayName || Component.name || "Component";
11+
const wrappedComponentName =
12+
Component.displayName || Component.name || "Component";
1213

1314
class AsyncScriptLoader extends React.Component {
1415
constructor() {
1516
super();
1617
this.state = {};
18+
this.__scriptURL = "";
1719
}
1820

1921
asyncScriptLoaderGetScriptLoaderID() {
@@ -23,11 +25,33 @@ export default function makeAsyncScript(Component, scriptURL, options) {
2325
return this.__scriptLoaderID;
2426
}
2527

28+
setupScriptURL() {
29+
this.__scriptURL =
30+
typeof getScriptURL === "function" ? getScriptURL() : getScriptURL;
31+
return this.__scriptURL;
32+
}
33+
2634
getComponent() {
27-
return this.childComponent;
35+
return this.__childComponent;
36+
}
37+
38+
asyncScriptLoaderHandleLoad(state) {
39+
this.setState(state, this.props.asyncScriptOnLoad);
40+
}
41+
42+
asyncScriptLoaderTriggerOnScriptLoaded() {
43+
let mapEntry = SCRIPT_MAP[this.__scriptURL];
44+
if (!mapEntry || !mapEntry.loaded) {
45+
throw new Error("Script is not loaded.");
46+
}
47+
for (let obsKey in mapEntry.observers) {
48+
mapEntry.observers[obsKey](mapEntry);
49+
}
50+
delete window[options.callbackName];
2851
}
2952

3053
componentDidMount() {
54+
const scriptURL = this.setupScriptURL();
3155
const key = this.asyncScriptLoaderGetScriptLoaderID();
3256
const { globalName, callbackName } = options;
3357
if (globalName && typeof window[globalName] !== "undefined") {
@@ -40,12 +64,12 @@ export default function makeAsyncScript(Component, scriptURL, options) {
4064
this.asyncScriptLoaderHandleLoad(entry);
4165
return;
4266
}
43-
entry.observers[key] = (entry) => this.asyncScriptLoaderHandleLoad(entry);
67+
entry.observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry);
4468
return;
4569
}
4670

4771
let observers = {};
48-
observers[key] = (entry) => this.asyncScriptLoaderHandleLoad(entry);
72+
observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry);
4973
SCRIPT_MAP[scriptURL] = {
5074
loaded: false,
5175
observers,
@@ -54,9 +78,9 @@ export default function makeAsyncScript(Component, scriptURL, options) {
5478
let script = document.createElement("script");
5579

5680
script.src = scriptURL;
57-
script.async = 1;
81+
script.async = true;
5882

59-
let callObserverFuncAndRemoveObserver = (func) => {
83+
let callObserverFuncAndRemoveObserver = func => {
6084
if (SCRIPT_MAP[scriptURL]) {
6185
let mapEntry = SCRIPT_MAP[scriptURL];
6286
let observersMap = mapEntry.observers;
@@ -70,14 +94,15 @@ export default function makeAsyncScript(Component, scriptURL, options) {
7094
};
7195

7296
if (callbackName && typeof window !== "undefined") {
73-
window[callbackName] = AsyncScriptLoader.asyncScriptLoaderTriggerOnScriptLoaded;
97+
window[callbackName] = () =>
98+
this.asyncScriptLoaderTriggerOnScriptLoaded();
7499
}
75100

76101
script.onload = () => {
77102
let mapEntry = SCRIPT_MAP[scriptURL];
78103
if (mapEntry) {
79104
mapEntry.loaded = true;
80-
callObserverFuncAndRemoveObserver( (observer) => {
105+
callObserverFuncAndRemoveObserver(observer => {
81106
if (callbackName) {
82107
return false;
83108
}
@@ -86,11 +111,11 @@ export default function makeAsyncScript(Component, scriptURL, options) {
86111
});
87112
}
88113
};
89-
script.onerror = (event) => {
114+
script.onerror = event => {
90115
let mapEntry = SCRIPT_MAP[scriptURL];
91116
if (mapEntry) {
92117
mapEntry.errored = true;
93-
callObserverFuncAndRemoveObserver( (observer) => {
118+
callObserverFuncAndRemoveObserver(observer => {
94119
observer(mapEntry);
95120
return true;
96121
});
@@ -113,15 +138,12 @@ export default function makeAsyncScript(Component, scriptURL, options) {
113138
document.body.appendChild(script);
114139
}
115140

116-
asyncScriptLoaderHandleLoad(state) {
117-
this.setState(state, this.props.asyncScriptOnLoad);
118-
}
119-
120141
componentWillUnmount() {
121142
// Remove tag script
143+
const scriptURL = this.__scriptURL;
122144
if (options.removeOnUnmount === true) {
123145
const allScripts = document.getElementsByTagName("script");
124-
for(let i = 0; i < allScripts.length; i += 1) {
146+
for (let i = 0; i < allScripts.length; i += 1) {
125147
if (allScripts[i].src.indexOf(scriptURL) > -1) {
126148
if (allScripts[i].parentNode) {
127149
allScripts[i].parentNode.removeChild(allScripts[i]);
@@ -144,25 +166,25 @@ export default function makeAsyncScript(Component, scriptURL, options) {
144166
// remove asyncScriptOnLoad from childprops
145167
let { asyncScriptOnLoad, ...childProps } = this.props;
146168
if (globalName && typeof window !== "undefined") {
147-
childProps[globalName] = typeof window[globalName] !== "undefined" ? window[globalName] : undefined;
169+
childProps[globalName] =
170+
typeof window[globalName] !== "undefined"
171+
? window[globalName]
172+
: undefined;
148173
}
149-
return <Component ref={(comp) => {this.childComponent = comp; }} {...childProps} />;
174+
return (
175+
<Component
176+
ref={comp => {
177+
this.__childComponent = comp;
178+
}}
179+
{...childProps}
180+
/>
181+
);
150182
}
151183
}
152184
AsyncScriptLoader.displayName = `AsyncScriptLoader(${wrappedComponentName})`;
153185
AsyncScriptLoader.propTypes = {
154186
asyncScriptOnLoad: PropTypes.func,
155187
};
156-
AsyncScriptLoader.asyncScriptLoaderTriggerOnScriptLoaded = function() {
157-
let mapEntry = SCRIPT_MAP[scriptURL];
158-
if (!mapEntry || !mapEntry.loaded) {
159-
throw new Error("Script is not loaded.");
160-
}
161-
for (let obsKey in mapEntry.observers) {
162-
mapEntry.observers[obsKey](mapEntry);
163-
}
164-
delete window[options.callbackName];
165-
};
166188

167189
if (options.exposeFuncs) {
168190
options.exposeFuncs.forEach(funcToExpose => {

test/.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"mocha": true
44
},
55
"globals": {
6-
"assert": true,
6+
"assert": true
77
},
88
"rules": {
99
"no-script-url": 1,

test/async-script-loader-spec.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ class MockedComponent extends React.Component {
1414
}
1515
}
1616

17-
const hasScript = () => {
17+
const hasScript = (URL) => {
1818
const scripTags = document.getElementsByTagName("script");
1919
for (let i = 0; i < scripTags.length; i += 1) {
20-
if (scripTags[i].src.indexOf("http://example.com") > -1) {
20+
if (scripTags[i].src.indexOf(URL) > -1) {
2121
return true;
2222
}
2323
}
@@ -30,54 +30,71 @@ describe("AsyncScriptLoader", () => {
3030
});
3131

3232
it("should return a component that contains the passed component", () => {
33-
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com");
33+
const URL = "http://example.com";
34+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL);
3435
assert.equal(ComponentWrapper.displayName, "AsyncScriptLoader(MockedComponent)");
35-
let instance = ReactTestUtils.renderIntoDocument(
36+
const instance = ReactTestUtils.renderIntoDocument(
3637
<ComponentWrapper />
3738
);
3839
assert.ok(ReactTestUtils.isCompositeComponent(instance));
3940
assert.ok(ReactTestUtils.isCompositeComponentWithType(instance, ComponentWrapper));
4041
assert.isNotNull(ReactTestUtils.findRenderedComponentWithType(instance, MockedComponent));
42+
assert.equal(hasScript(URL), true);
4143
});
4244
it("should handle successfully already loaded global object", () => {
43-
let globalName = "SomeGlobal";
45+
const URL = "http://example.com";
46+
const globalName = "SomeGlobal";
4447
window[globalName] = {};
45-
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", { globalName: globalName });
46-
let instance = ReactTestUtils.renderIntoDocument(
48+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL, { globalName: globalName });
49+
const instance = ReactTestUtils.renderIntoDocument(
4750
<ComponentWrapper />
4851
);
52+
assert.equal(hasScript(URL), true);
4953
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
5054
instance.componentWillUnmount();
5155
delete window[globalName];
5256
});
5357

58+
it("should accept a function for scriptURL", () => {
59+
const URL = "http://example.com/?url=function";
60+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, () => URL);
61+
const instance = ReactTestUtils.renderIntoDocument(
62+
<ComponentWrapper />
63+
);
64+
assert.equal(hasScript(URL), true);
65+
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
66+
instance.componentWillUnmount();
67+
});
68+
5469
it("should expose functions with scope correctly", (done) => {
55-
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", {
70+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com/", {
5671
exposeFuncs: ["callsACallback"],
5772
});
58-
let instance = ReactTestUtils.renderIntoDocument(
73+
const instance = ReactTestUtils.renderIntoDocument(
5974
<ComponentWrapper />
6075
);
6176
instance.callsACallback(done);
6277
});
6378
it("should not remove tag script on removeOnUnmount option not set", () => {
64-
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com");
65-
let instance = ReactTestUtils.renderIntoDocument(
79+
const URL = "http://example.com/?removeOnUnmount=notset";
80+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL);
81+
const instance = ReactTestUtils.renderIntoDocument(
6682
<ComponentWrapper />
6783
);
68-
assert.equal(hasScript(), true);
84+
assert.equal(hasScript(URL), true);
6985
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
7086
instance.componentWillUnmount();
71-
assert.equal(hasScript(), true);
87+
assert.equal(hasScript(URL), true);
7288
});
7389
it("should remove tag script on removeOnUnmount option set to true", () => {
74-
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", { removeOnUnmount: true });
75-
let instance = ReactTestUtils.renderIntoDocument(
90+
const URL = "http://example.com/?removeOnUnmount=true";
91+
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL, { removeOnUnmount: true });
92+
const instance = ReactTestUtils.renderIntoDocument(
7693
<ComponentWrapper />
7794
);
78-
assert.equal(hasScript(), true);
95+
assert.equal(hasScript(URL), true);
7996
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
8097
instance.componentWillUnmount();
81-
assert.equal(hasScript(), false);
98+
assert.equal(hasScript(URL), false);
8299
});
83100
});

0 commit comments

Comments
 (0)