Skip to content

Dynamic URL building #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"no-multiple-empty-lines": [1, { "max": 1}],
"no-underscore-dangle": 0,
"no-unused-vars": [1, { "vars": "all", "args": "none" }],
"no-undef": 1,
"no-undef": 2,
"no-var": 2,
"quote-props": [2, "as-needed"],
"quotes": [2, "double"],
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ build/Release
node_modules

/lib/


# Editors
.vscode
3 changes: 2 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ tools/
.gitignore
.travis.yml
karma.conf.js
.babelrc
.babelrc
.vscode
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ I decided push react-script-loader a bit further and make a composition function

## Usage

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

- Component: The component to wrap.
- scriptUrl: the full of the script tag.
- getScriptUrl: a string or function that returns the full URL of the script tag.
- options *(optional)*:
- 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.
- callbackName: If the scripts calls a global function when loaded, provide the callback name here. It'll be autoregistered on the window.
Expand Down
78 changes: 50 additions & 28 deletions src/async-script-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ let SCRIPT_MAP = {};
// A counter used to generate a unique id for each component that uses the function
let idCount = 0;

export default function makeAsyncScript(Component, scriptURL, options) {
export default function makeAsyncScript(Component, getScriptURL, options) {
options = options || {};
const wrappedComponentName = Component.displayName || Component.name || "Component";
const wrappedComponentName =
Component.displayName || Component.name || "Component";

class AsyncScriptLoader extends React.Component {
constructor() {
super();
this.state = {};
this.__scriptURL = "";
}

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

setupScriptURL() {
this.__scriptURL =
typeof getScriptURL === "function" ? getScriptURL() : getScriptURL;
return this.__scriptURL;
}

getComponent() {
return this.childComponent;
return this.__childComponent;
}

asyncScriptLoaderHandleLoad(state) {
this.setState(state, this.props.asyncScriptOnLoad);
}

asyncScriptLoaderTriggerOnScriptLoaded() {
let mapEntry = SCRIPT_MAP[this.__scriptURL];
if (!mapEntry || !mapEntry.loaded) {
throw new Error("Script is not loaded.");
}
for (let obsKey in mapEntry.observers) {
mapEntry.observers[obsKey](mapEntry);
}
delete window[options.callbackName];
}

componentDidMount() {
const scriptURL = this.setupScriptURL();
const key = this.asyncScriptLoaderGetScriptLoaderID();
const { globalName, callbackName } = options;
if (globalName && typeof window[globalName] !== "undefined") {
Expand All @@ -40,12 +64,12 @@ export default function makeAsyncScript(Component, scriptURL, options) {
this.asyncScriptLoaderHandleLoad(entry);
return;
}
entry.observers[key] = (entry) => this.asyncScriptLoaderHandleLoad(entry);
entry.observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry);
return;
}

let observers = {};
observers[key] = (entry) => this.asyncScriptLoaderHandleLoad(entry);
observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry);
SCRIPT_MAP[scriptURL] = {
loaded: false,
observers,
Expand All @@ -54,9 +78,9 @@ export default function makeAsyncScript(Component, scriptURL, options) {
let script = document.createElement("script");

script.src = scriptURL;
script.async = 1;
script.async = true;

let callObserverFuncAndRemoveObserver = (func) => {
let callObserverFuncAndRemoveObserver = func => {
if (SCRIPT_MAP[scriptURL]) {
let mapEntry = SCRIPT_MAP[scriptURL];
let observersMap = mapEntry.observers;
Expand All @@ -70,14 +94,15 @@ export default function makeAsyncScript(Component, scriptURL, options) {
};

if (callbackName && typeof window !== "undefined") {
window[callbackName] = AsyncScriptLoader.asyncScriptLoaderTriggerOnScriptLoaded;
window[callbackName] = () =>
this.asyncScriptLoaderTriggerOnScriptLoaded();
}

script.onload = () => {
let mapEntry = SCRIPT_MAP[scriptURL];
if (mapEntry) {
mapEntry.loaded = true;
callObserverFuncAndRemoveObserver( (observer) => {
callObserverFuncAndRemoveObserver(observer => {
if (callbackName) {
return false;
}
Expand All @@ -86,11 +111,11 @@ export default function makeAsyncScript(Component, scriptURL, options) {
});
}
};
script.onerror = (event) => {
script.onerror = event => {
let mapEntry = SCRIPT_MAP[scriptURL];
if (mapEntry) {
mapEntry.errored = true;
callObserverFuncAndRemoveObserver( (observer) => {
callObserverFuncAndRemoveObserver(observer => {
observer(mapEntry);
return true;
});
Expand All @@ -113,15 +138,12 @@ export default function makeAsyncScript(Component, scriptURL, options) {
document.body.appendChild(script);
}

asyncScriptLoaderHandleLoad(state) {
this.setState(state, this.props.asyncScriptOnLoad);
}

componentWillUnmount() {
// Remove tag script
const scriptURL = this.__scriptURL;
if (options.removeOnUnmount === true) {
const allScripts = document.getElementsByTagName("script");
for(let i = 0; i < allScripts.length; i += 1) {
for (let i = 0; i < allScripts.length; i += 1) {
if (allScripts[i].src.indexOf(scriptURL) > -1) {
if (allScripts[i].parentNode) {
allScripts[i].parentNode.removeChild(allScripts[i]);
Expand All @@ -144,25 +166,25 @@ export default function makeAsyncScript(Component, scriptURL, options) {
// remove asyncScriptOnLoad from childprops
let { asyncScriptOnLoad, ...childProps } = this.props;
if (globalName && typeof window !== "undefined") {
childProps[globalName] = typeof window[globalName] !== "undefined" ? window[globalName] : undefined;
childProps[globalName] =
typeof window[globalName] !== "undefined"
? window[globalName]
: undefined;
}
return <Component ref={(comp) => {this.childComponent = comp; }} {...childProps} />;
return (
<Component
ref={comp => {
this.__childComponent = comp;
}}
{...childProps}
/>
);
}
}
AsyncScriptLoader.displayName = `AsyncScriptLoader(${wrappedComponentName})`;
AsyncScriptLoader.propTypes = {
asyncScriptOnLoad: PropTypes.func,
};
AsyncScriptLoader.asyncScriptLoaderTriggerOnScriptLoaded = function() {
let mapEntry = SCRIPT_MAP[scriptURL];
if (!mapEntry || !mapEntry.loaded) {
throw new Error("Script is not loaded.");
}
for (let obsKey in mapEntry.observers) {
mapEntry.observers[obsKey](mapEntry);
}
delete window[options.callbackName];
};

if (options.exposeFuncs) {
options.exposeFuncs.forEach(funcToExpose => {
Expand Down
2 changes: 1 addition & 1 deletion test/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"mocha": true
},
"globals": {
"assert": true,
"assert": true
},
"rules": {
"no-script-url": 1,
Expand Down
51 changes: 34 additions & 17 deletions test/async-script-loader-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ class MockedComponent extends React.Component {
}
}

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

it("should return a component that contains the passed component", () => {
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com");
const URL = "http://example.com";
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL);
assert.equal(ComponentWrapper.displayName, "AsyncScriptLoader(MockedComponent)");
let instance = ReactTestUtils.renderIntoDocument(
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
assert.ok(ReactTestUtils.isCompositeComponent(instance));
assert.ok(ReactTestUtils.isCompositeComponentWithType(instance, ComponentWrapper));
assert.isNotNull(ReactTestUtils.findRenderedComponentWithType(instance, MockedComponent));
assert.equal(hasScript(URL), true);
});
it("should handle successfully already loaded global object", () => {
let globalName = "SomeGlobal";
const URL = "http://example.com";
const globalName = "SomeGlobal";
window[globalName] = {};
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", { globalName: globalName });
let instance = ReactTestUtils.renderIntoDocument(
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL, { globalName: globalName });
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
assert.equal(hasScript(URL), true);
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
instance.componentWillUnmount();
delete window[globalName];
});

it("should accept a function for scriptURL", () => {
const URL = "http://example.com/?url=function";
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, () => URL);
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
assert.equal(hasScript(URL), true);
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
instance.componentWillUnmount();
});

it("should expose functions with scope correctly", (done) => {
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", {
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com/", {
exposeFuncs: ["callsACallback"],
});
let instance = ReactTestUtils.renderIntoDocument(
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
instance.callsACallback(done);
});
it("should not remove tag script on removeOnUnmount option not set", () => {
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com");
let instance = ReactTestUtils.renderIntoDocument(
const URL = "http://example.com/?removeOnUnmount=notset";
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL);
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
assert.equal(hasScript(), true);
assert.equal(hasScript(URL), true);
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
instance.componentWillUnmount();
assert.equal(hasScript(), true);
assert.equal(hasScript(URL), true);
});
it("should remove tag script on removeOnUnmount option set to true", () => {
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", { removeOnUnmount: true });
let instance = ReactTestUtils.renderIntoDocument(
const URL = "http://example.com/?removeOnUnmount=true";
const ComponentWrapper = makeAsyncScriptLoader(MockedComponent, URL, { removeOnUnmount: true });
const instance = ReactTestUtils.renderIntoDocument(
<ComponentWrapper />
);
assert.equal(hasScript(), true);
assert.equal(hasScript(URL), true);
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(instance));
instance.componentWillUnmount();
assert.equal(hasScript(), false);
assert.equal(hasScript(URL), false);
});
});