react-async-script
Advanced tools
Comparing version 0.11.1 to 1.0.0-rc.1
@@ -0,1 +1,13 @@ | ||
v1.0.0-rc.1 - Sun 12 Aug 2018 17:47:00 PST | ||
-------------------------------------- | ||
- React forward ref [(#37)](https://github.com/dozoisch/react-async-script/pull/37) | ||
- Update to react 16.4.1 [(#37)](https://github.com/dozoisch/react-async-script/pull/37) | ||
- Hoist non react statics [(#35)](https://github.com/dozoisch/react-async-script/pull/35) | ||
- Updated Travis Node versions [(#36)](https://github.com/dozoisch/react-async-script/pull/36) | ||
- Refactor to new HOC pattern [(#34)](https://github.com/dozoisch/react-async-script/pull/34) | ||
- Remove old broken IE support [(#34)](https://github.com/dozoisch/react-async-script/pull/34) | ||
v0.11.1 - Sat, 4 Aug 2018 12:46:00 PST | ||
@@ -8,2 +20,3 @@ -------------------------------------- | ||
v0.11.0 - Sun, 29 Jul 2018 11:58:00 PST | ||
@@ -10,0 +23,0 @@ -------------------------------------- |
@@ -11,4 +11,2 @@ "use strict"; | ||
var _react2 = _interopRequireDefault(_react); | ||
var _propTypes = require("prop-types"); | ||
@@ -18,2 +16,6 @@ | ||
var _hoistNonReactStatics = require("hoist-non-react-statics"); | ||
var _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -34,209 +36,205 @@ | ||
function makeAsyncScript(Component, getScriptURL, options) { | ||
function makeAsyncScript(getScriptURL, options) { | ||
options = options || {}; | ||
var wrappedComponentName = Component.displayName || Component.name || "Component"; | ||
return function wrapWithAsyncScript(WrappedComponent) { | ||
var wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || "Component"; | ||
var AsyncScriptLoader = function (_React$Component) { | ||
_inherits(AsyncScriptLoader, _React$Component); | ||
var AsyncScriptLoader = function (_Component) { | ||
_inherits(AsyncScriptLoader, _Component); | ||
function AsyncScriptLoader() { | ||
_classCallCheck(this, AsyncScriptLoader); | ||
function AsyncScriptLoader(props, context) { | ||
_classCallCheck(this, AsyncScriptLoader); | ||
var _this = _possibleConstructorReturn(this, _React$Component.call(this)); | ||
var _this = _possibleConstructorReturn(this, _Component.call(this, props, context)); | ||
_this.state = {}; | ||
_this.__scriptURL = ""; | ||
return _this; | ||
} | ||
AsyncScriptLoader.prototype.asyncScriptLoaderGetScriptLoaderID = function asyncScriptLoaderGetScriptLoaderID() { | ||
if (!this.__scriptLoaderID) { | ||
this.__scriptLoaderID = "async-script-loader-" + idCount++; | ||
_this.state = {}; | ||
_this.__scriptURL = ""; | ||
return _this; | ||
} | ||
return this.__scriptLoaderID; | ||
}; | ||
AsyncScriptLoader.prototype.setupScriptURL = function setupScriptURL() { | ||
this.__scriptURL = typeof getScriptURL === "function" ? getScriptURL() : getScriptURL; | ||
return this.__scriptURL; | ||
}; | ||
AsyncScriptLoader.prototype.asyncScriptLoaderGetScriptLoaderID = function asyncScriptLoaderGetScriptLoaderID() { | ||
if (!this.__scriptLoaderID) { | ||
this.__scriptLoaderID = "async-script-loader-" + idCount++; | ||
} | ||
return this.__scriptLoaderID; | ||
}; | ||
AsyncScriptLoader.prototype.getComponent = function getComponent() { | ||
return this.__childComponent; | ||
}; | ||
AsyncScriptLoader.prototype.setupScriptURL = function setupScriptURL() { | ||
this.__scriptURL = typeof getScriptURL === "function" ? getScriptURL() : getScriptURL; | ||
return this.__scriptURL; | ||
}; | ||
AsyncScriptLoader.prototype.asyncScriptLoaderHandleLoad = function asyncScriptLoaderHandleLoad(state) { | ||
this.setState(state, this.props.asyncScriptOnLoad); | ||
}; | ||
AsyncScriptLoader.prototype.asyncScriptLoaderHandleLoad = function asyncScriptLoaderHandleLoad(state) { | ||
var _this2 = this; | ||
AsyncScriptLoader.prototype.asyncScriptLoaderTriggerOnScriptLoaded = function asyncScriptLoaderTriggerOnScriptLoaded() { | ||
var mapEntry = SCRIPT_MAP[this.__scriptURL]; | ||
if (!mapEntry || !mapEntry.loaded) { | ||
throw new Error("Script is not loaded."); | ||
} | ||
for (var obsKey in mapEntry.observers) { | ||
mapEntry.observers[obsKey](mapEntry); | ||
} | ||
delete window[options.callbackName]; | ||
}; | ||
// use reacts setState callback to fire props.asyncScriptOnLoad with new state/entry | ||
this.setState(state, function () { | ||
return _this2.props.asyncScriptOnLoad && _this2.props.asyncScriptOnLoad(_this2.state); | ||
}); | ||
}; | ||
AsyncScriptLoader.prototype.componentDidMount = function componentDidMount() { | ||
var _this2 = this; | ||
AsyncScriptLoader.prototype.asyncScriptLoaderTriggerOnScriptLoaded = function asyncScriptLoaderTriggerOnScriptLoaded() { | ||
var mapEntry = SCRIPT_MAP[this.__scriptURL]; | ||
if (!mapEntry || !mapEntry.loaded) { | ||
throw new Error("Script is not loaded."); | ||
} | ||
for (var obsKey in mapEntry.observers) { | ||
mapEntry.observers[obsKey](mapEntry); | ||
} | ||
delete window[options.callbackName]; | ||
}; | ||
var scriptURL = this.setupScriptURL(); | ||
var key = this.asyncScriptLoaderGetScriptLoaderID(); | ||
var _options = options, | ||
globalName = _options.globalName, | ||
callbackName = _options.callbackName; | ||
AsyncScriptLoader.prototype.componentDidMount = function componentDidMount() { | ||
var _this3 = this; | ||
if (globalName && typeof window[globalName] !== "undefined") { | ||
SCRIPT_MAP[scriptURL] = { loaded: true, observers: {} }; | ||
} | ||
var scriptURL = this.setupScriptURL(); | ||
var key = this.asyncScriptLoaderGetScriptLoaderID(); | ||
var _options = options, | ||
globalName = _options.globalName, | ||
callbackName = _options.callbackName; | ||
if (SCRIPT_MAP[scriptURL]) { | ||
var entry = SCRIPT_MAP[scriptURL]; | ||
if (entry && (entry.loaded || entry.errored)) { | ||
this.asyncScriptLoaderHandleLoad(entry); | ||
// check if global object already attached to window | ||
if (globalName && typeof window[globalName] !== "undefined") { | ||
SCRIPT_MAP[scriptURL] = { loaded: true, observers: {} }; | ||
} | ||
// check if script loading already | ||
if (SCRIPT_MAP[scriptURL]) { | ||
var entry = SCRIPT_MAP[scriptURL]; | ||
// if loaded or errored then "finish" | ||
if (entry && (entry.loaded || entry.errored)) { | ||
this.asyncScriptLoaderHandleLoad(entry); | ||
return; | ||
} | ||
// if still loading then callback to observer queue | ||
entry.observers[key] = function (entry) { | ||
return _this3.asyncScriptLoaderHandleLoad(entry); | ||
}; | ||
return; | ||
} | ||
entry.observers[key] = function (entry) { | ||
return _this2.asyncScriptLoaderHandleLoad(entry); | ||
/* | ||
* hasn't started loading | ||
* start the "magic" | ||
* setup script to load and observers | ||
*/ | ||
var observers = {}; | ||
observers[key] = function (entry) { | ||
return _this3.asyncScriptLoaderHandleLoad(entry); | ||
}; | ||
return; | ||
} | ||
SCRIPT_MAP[scriptURL] = { | ||
loaded: false, | ||
observers: observers | ||
}; | ||
var observers = {}; | ||
observers[key] = function (entry) { | ||
return _this2.asyncScriptLoaderHandleLoad(entry); | ||
}; | ||
SCRIPT_MAP[scriptURL] = { | ||
loaded: false, | ||
observers: observers | ||
}; | ||
var script = document.createElement("script"); | ||
var script = document.createElement("script"); | ||
script.src = scriptURL; | ||
script.async = true; | ||
script.src = scriptURL; | ||
script.async = true; | ||
var callObserverFuncAndRemoveObserver = function callObserverFuncAndRemoveObserver(func) { | ||
if (SCRIPT_MAP[scriptURL]) { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
var observersMap = mapEntry.observers; | ||
var callObserverFuncAndRemoveObserver = function callObserverFuncAndRemoveObserver(func) { | ||
if (SCRIPT_MAP[scriptURL]) { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
var observersMap = mapEntry.observers; | ||
for (var obsKey in observersMap) { | ||
if (func(observersMap[obsKey])) { | ||
delete observersMap[obsKey]; | ||
for (var obsKey in observersMap) { | ||
if (func(observersMap[obsKey])) { | ||
delete observersMap[obsKey]; | ||
} | ||
} | ||
} | ||
}; | ||
if (callbackName && typeof window !== "undefined") { | ||
window[callbackName] = function () { | ||
return _this3.asyncScriptLoaderTriggerOnScriptLoaded(); | ||
}; | ||
} | ||
}; | ||
if (callbackName && typeof window !== "undefined") { | ||
window[callbackName] = function () { | ||
return _this2.asyncScriptLoaderTriggerOnScriptLoaded(); | ||
script.onload = function () { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.loaded = true; | ||
callObserverFuncAndRemoveObserver(function (observer) { | ||
if (callbackName) { | ||
return false; | ||
} | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
}; | ||
} | ||
script.onerror = function (event) { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.errored = true; | ||
callObserverFuncAndRemoveObserver(function (observer) { | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
}; | ||
script.onload = function () { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.loaded = true; | ||
callObserverFuncAndRemoveObserver(function (observer) { | ||
if (callbackName) { | ||
return false; | ||
document.body.appendChild(script); | ||
}; | ||
AsyncScriptLoader.prototype.componentWillUnmount = function componentWillUnmount() { | ||
// Remove tag script | ||
var scriptURL = this.__scriptURL; | ||
if (options.removeOnUnmount === true) { | ||
var allScripts = document.getElementsByTagName("script"); | ||
for (var i = 0; i < allScripts.length; i += 1) { | ||
if (allScripts[i].src.indexOf(scriptURL) > -1) { | ||
if (allScripts[i].parentNode) { | ||
allScripts[i].parentNode.removeChild(allScripts[i]); | ||
} | ||
} | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
} | ||
}; | ||
script.onerror = function (event) { | ||
// Clean the observer entry | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.errored = true; | ||
callObserverFuncAndRemoveObserver(function (observer) { | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
delete mapEntry.observers[this.asyncScriptLoaderGetScriptLoaderID()]; | ||
if (options.removeOnUnmount === true) { | ||
delete SCRIPT_MAP[scriptURL]; | ||
} | ||
} | ||
}; | ||
// (old) MSIE browsers may call "onreadystatechange" instead of "onload" | ||
script.onreadystatechange = function () { | ||
if (_this2.readyState === "loaded") { | ||
// wait for other events, then call onload if default onload hadn't been called | ||
window.setTimeout(function () { | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry && mapEntry.loaded !== true) { | ||
script.onload(); | ||
} | ||
}, 0); | ||
} | ||
}; | ||
AsyncScriptLoader.prototype.render = function render() { | ||
var globalName = options.globalName; | ||
// remove asyncScriptOnLoad from childProps | ||
document.body.appendChild(script); | ||
}; | ||
var _props = this.props, | ||
asyncScriptOnLoad = _props.asyncScriptOnLoad, | ||
forwardedRef = _props.forwardedRef, | ||
childProps = _objectWithoutProperties(_props, ["asyncScriptOnLoad", "forwardedRef"]); // eslint-disable-line no-unused-vars | ||
AsyncScriptLoader.prototype.componentWillUnmount = function componentWillUnmount() { | ||
// Remove tag script | ||
var scriptURL = this.__scriptURL; | ||
if (options.removeOnUnmount === true) { | ||
var allScripts = document.getElementsByTagName("script"); | ||
for (var i = 0; i < allScripts.length; i += 1) { | ||
if (allScripts[i].src.indexOf(scriptURL) > -1) { | ||
if (allScripts[i].parentNode) { | ||
allScripts[i].parentNode.removeChild(allScripts[i]); | ||
} | ||
} | ||
if (globalName && typeof window !== "undefined") { | ||
childProps[globalName] = typeof window[globalName] !== "undefined" ? window[globalName] : undefined; | ||
} | ||
} | ||
// Clean the observer entry | ||
var mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
delete mapEntry.observers[this.asyncScriptLoaderGetScriptLoaderID()]; | ||
if (options.removeOnUnmount === true) { | ||
delete SCRIPT_MAP[scriptURL]; | ||
} | ||
} | ||
}; | ||
childProps.ref = forwardedRef; | ||
return (0, _react.createElement)(WrappedComponent, childProps); | ||
}; | ||
AsyncScriptLoader.prototype.render = function render() { | ||
var _this3 = this; | ||
return AsyncScriptLoader; | ||
}(_react.Component); | ||
var globalName = options.globalName; | ||
// remove asyncScriptOnLoad from childprops | ||
// Note the second param "ref" provided by React.forwardRef. | ||
// We can pass it along to AsyncScriptLoader as a regular prop, e.g. "forwardedRef" | ||
// And it can then be attached to the Component. | ||
var _props = this.props, | ||
asyncScriptOnLoad = _props.asyncScriptOnLoad, | ||
childProps = _objectWithoutProperties(_props, ["asyncScriptOnLoad"]); | ||
if (globalName && typeof window !== "undefined") { | ||
childProps[globalName] = typeof window[globalName] !== "undefined" ? window[globalName] : undefined; | ||
} | ||
return _react2.default.createElement(Component, _extends({ | ||
ref: function ref(comp) { | ||
_this3.__childComponent = comp; | ||
} | ||
}, childProps)); | ||
var ForwardedComponent = (0, _react.forwardRef)(function (props, ref) { | ||
return (0, _react.createElement)(AsyncScriptLoader, _extends({}, props, { forwardedRef: ref })); | ||
}); | ||
ForwardedComponent.displayName = "AsyncScriptLoader(" + wrappedComponentName + ")"; | ||
ForwardedComponent.propTypes = { | ||
asyncScriptOnLoad: _propTypes2.default.func | ||
}; | ||
return AsyncScriptLoader; | ||
}(_react2.default.Component); | ||
AsyncScriptLoader.displayName = "AsyncScriptLoader(" + wrappedComponentName + ")"; | ||
AsyncScriptLoader.propTypes = { | ||
asyncScriptOnLoad: _propTypes2.default.func | ||
return (0, _hoistNonReactStatics2.default)(ForwardedComponent, WrappedComponent); | ||
}; | ||
if (options.exposeFuncs) { | ||
options.exposeFuncs.forEach(function (funcToExpose) { | ||
AsyncScriptLoader.prototype[funcToExpose] = function () { | ||
var _getComponent; | ||
return (_getComponent = this.getComponent())[funcToExpose].apply(_getComponent, arguments); | ||
}; | ||
}); | ||
} | ||
return AsyncScriptLoader; | ||
} |
{ | ||
"name": "react-async-script", | ||
"version": "0.11.1", | ||
"version": "1.0.0-rc.1", | ||
"description": "A composition mixin for loading scripts asynchronously for React", | ||
@@ -41,2 +41,3 @@ "main": "lib/async-script-loader.js", | ||
"es5-shim": "~4.1.3", | ||
"es6-shim": "^0.35.3", | ||
"eslint": "~1.6.0", | ||
@@ -55,13 +56,15 @@ "eslint-config-defaults": "~7.0.1", | ||
"mocha": "~2.3.3", | ||
"phantomjs": "^1.9.18", | ||
"react": "^15.5.0", | ||
"react-dom": "^15.5.0", | ||
"phantomjs": "^2.0.0", | ||
"react": "^16.4.2", | ||
"react-dom": "^16.4.2", | ||
"react-is": "^16.4.2", | ||
"webpack": "~1.14.0" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=15.5.0" | ||
"react": ">=16.4.1" | ||
}, | ||
"dependencies": { | ||
"prop-types": ">=15.5.0" | ||
"hoist-non-react-statics": "^3.0.1", | ||
"prop-types": "^15.5.0" | ||
} | ||
} |
142
README.md
@@ -5,19 +5,53 @@ # React Async Script Loader | ||
A React composition mixin for loading 3rd party scripts asynchronously. This component allows you to wrap component | ||
that needs 3rd party resources, like reCAPTCHA or Google Maps, and have them load the script asynchronously. | ||
A React HOC for loading 3rd party scripts asynchronously. This HOC allows you to wrap a component that needs 3rd party resources, like reCAPTCHA or Google Maps, and have them load the script asynchronously. | ||
## Usage | ||
The api is very simple `makeAsyncScriptLoader(Component, getScriptUrl, options)`. Where options can contain exposeFuncs, callbackName and globalName. | ||
#### Async Script HOC api | ||
- `Component`: The component to wrap. | ||
- `getScriptUrl`: a string or function that returns the full URL of the script tag. | ||
- options *(optional)*: | ||
- `exposeFuncs`: Array of Strings. 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. | ||
- `globalName`: If wanted, provide the globalName of the loaded script. It'll be injected on the component with the same name *(ex: "grecaptcha")* | ||
- `removeOnUnmount`: Boolean **default=false**: If set to true removes the script tag on the component unmount | ||
`makeAsyncScriptLoader(getScriptUrl, options)(Component)` | ||
You can retrieve the child component using the function called `getComponent()`. | ||
- `Component`: The *Component* to wrap. | ||
- `getScriptUrl`: *string* or *function* that returns the full URL of the script tag. | ||
- `options` *(optional)*: | ||
- `callbackName`: *string* : If the script needs to call a global function when finished loading *(for example: `recaptcha/api.js?onload=callbackName`)*. Please provide the callback name here and it will be autoregistered on `window` for you. | ||
- `globalName`: *string* : Can provide the name of the global that the script attaches to `window`. Async-script will pass this as a prop to the wrapped component. *(`props[globalName] = window[globalName]`)* | ||
- `removeOnUnmount`: *boolean* **default=false** : If set to `true` removes the script tag when component unmounts. | ||
#### HOC Component props | ||
``` | ||
const AsyncScriptComponent = makeAsyncScriptLoader(URL)(Component); | ||
--- | ||
<AsyncScriptComponent asyncScriptOnLoad={callAfterScriptLoads} /> | ||
``` | ||
- `asyncScriptOnLoad`: *function* : called after script finishes loading. *using `script.onload`* | ||
#### Ref and forwardRef | ||
`react-async-script` uses react's `forwardRef` method to pass along the `ref` applied to the wrapped component. | ||
If you pass a `ref` prop you'll have access to your wrapped components instance. See the tests for detailed example. | ||
Simple Example: | ||
``` | ||
const AsyncHoc = makeAsyncScriptLoader(URL)(ComponentNeedsScript); | ||
class DisplayComponent extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this._internalRef = React.createRef(); | ||
} | ||
componentDidMount() { | ||
console.log("ComponentNeedsScript's Instance -", this._internalRef.current); | ||
} | ||
render() { return (<AsyncHoc ref={this._internalRef} />)} | ||
} | ||
``` | ||
##### Notes on Requirements | ||
At least `React@16.4.1` is required due to `forwardRef` usage internally. | ||
### Example | ||
@@ -28,34 +62,36 @@ | ||
```js | ||
// recaptcha.js | ||
export class ReCAPTCHA extends React.Component { | ||
componentDidUpdate(prevProps) { | ||
// recaptcha has loaded via async script | ||
if (!prevProps.grecaptcha && this.props.grecaptcha) { | ||
this.props.grecaptcha.render(this._container) | ||
} | ||
} | ||
render() { return ( | ||
<div ref={(r) => this._container = r} />) | ||
} | ||
} | ||
// recaptcha-wrapper.js | ||
import React from "react"; | ||
import makeAsyncScriptLoader from "react-async-script"; | ||
import { ReCAPTCHA } from "./recaptcha"; | ||
import ReCAPTCHA from "./recaptcha"; | ||
import makeAsyncScriptLoader from "./react-async-script"; | ||
const callbackName = "onloadcallback"; | ||
const URL = `https://www.google.com/recaptcha/api.js?onload=${callbackName}&render=explicit`; | ||
// the name of the global that recaptcha/api.js sets on window ie: window.grecaptcha | ||
const globalName = "grecaptcha"; | ||
export default makeAsyncScriptLoader(ReCAPTCHA, URL, { | ||
export default makeAsyncScriptLoader(URL, { | ||
callbackName: callbackName, | ||
globalName: globalName, | ||
}); | ||
})(ReCAPTCHA); | ||
// main.js | ||
import React from "react"; | ||
import ReCAPTCHAWrapper from "./recaptcha-wrapper.js" | ||
function onLoad() { | ||
console.log("script loaded"); | ||
} | ||
const onLoad = () => console.log("script loaded") | ||
let reCAPTCHAprops = { | ||
siteKey: "xxxxxxx", | ||
//... | ||
}; | ||
React.render( | ||
<ReCAPTCHAWrapper asyncScriptOnLoad={onLoad} {...reCAPTCHAprops} />, | ||
<ReCAPTCHAWrapper asyncScriptOnLoad={onLoad} />, | ||
document.body | ||
@@ -65,52 +101,8 @@ ); | ||
## Expose Functions | ||
This is really useful if the child component has some utility functions (like `getValue`) that you would like the wrapper to expose. | ||
You can still retrieve the child component using `getComponent()`. | ||
### Example | ||
```js | ||
const MockedComponent = React.createClass({ | ||
displayName: "MockedComponent", | ||
callsACallback(fn) { | ||
fn(); | ||
}, | ||
render() { | ||
return <span/>; | ||
} | ||
}); | ||
let ComponentWrapper = makeAsyncScriptLoader(MockedComponent, "http://example.com", { | ||
exposeFuncs: ["callsACallback"] | ||
}); | ||
let instance = ReactTestUtils.renderIntoDocument( | ||
<ComponentWrapper /> | ||
); | ||
instance.callsACallback(function () { console.log("Called from child", this.constructor.displayName); }); | ||
``` | ||
## Notes | ||
### History | ||
Pre `1.0.0` and - `React < 15.5.*` support details in [0.11.1](https://github.com/dozoisch/react-async-script/tree/v0.11.1). | ||
With React 0.13, mixins are getting deprecated in favor of composition. | ||
After reading this article, [Mixins Are Dead. Long Live Composition][dan_abramov], | ||
I decided push react-script-loader a bit further and make a composition function that wraps component. | ||
### Version to use | ||
- __React < 15.5__: v0.8.0 | ||
- __React >= 15.5__: >= v0.9.0 | ||
--- | ||
*Inspired by [react-script-loader][sl]* | ||
*The build tools are highly inspired by [react-bootstrap][rb]* | ||
[travis.img]: https://travis-ci.org/dozoisch/react-async-script.svg?branch=master | ||
@@ -124,5 +116,1 @@ [travis.url]: https://travis-ci.org/dozoisch/react-async-script | ||
[deps.url]: https://david-dm.org/dozoisch/react-async-script | ||
[dan_abramov]: https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750 | ||
[sl]: https://github.com/yariv/ReactScriptLoader | ||
[rb]: https://github.com/react-bootstrap/react-bootstrap/ |
@@ -1,3 +0,4 @@ | ||
import React from "react"; | ||
import { Component, createElement, forwardRef } from "react"; | ||
import PropTypes from "prop-types"; | ||
import hoistStatics from "hoist-non-react-statics"; | ||
@@ -9,190 +10,182 @@ let SCRIPT_MAP = {}; | ||
export default function makeAsyncScript(Component, getScriptURL, options) { | ||
export default function makeAsyncScript(getScriptURL, options) { | ||
options = options || {}; | ||
const wrappedComponentName = | ||
Component.displayName || Component.name || "Component"; | ||
return function wrapWithAsyncScript(WrappedComponent) { | ||
const wrappedComponentName = | ||
WrappedComponent.displayName || WrappedComponent.name || "Component"; | ||
class AsyncScriptLoader extends React.Component { | ||
constructor() { | ||
super(); | ||
this.state = {}; | ||
this.__scriptURL = ""; | ||
} | ||
class AsyncScriptLoader extends Component { | ||
constructor(props, context) { | ||
super(props, context) | ||
this.state = {}; | ||
this.__scriptURL = ""; | ||
} | ||
asyncScriptLoaderGetScriptLoaderID() { | ||
if (!this.__scriptLoaderID) { | ||
this.__scriptLoaderID = "async-script-loader-" + idCount++; | ||
asyncScriptLoaderGetScriptLoaderID() { | ||
if (!this.__scriptLoaderID) { | ||
this.__scriptLoaderID = "async-script-loader-" + idCount++; | ||
} | ||
return this.__scriptLoaderID; | ||
} | ||
return this.__scriptLoaderID; | ||
} | ||
setupScriptURL() { | ||
this.__scriptURL = | ||
typeof getScriptURL === "function" ? getScriptURL() : getScriptURL; | ||
return this.__scriptURL; | ||
} | ||
setupScriptURL() { | ||
this.__scriptURL = | ||
typeof getScriptURL === "function" ? getScriptURL() : getScriptURL; | ||
return this.__scriptURL; | ||
} | ||
getComponent() { | ||
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."); | ||
asyncScriptLoaderHandleLoad(state) { | ||
// use reacts setState callback to fire props.asyncScriptOnLoad with new state/entry | ||
this.setState(state, | ||
() => this.props.asyncScriptOnLoad && this.props.asyncScriptOnLoad(this.state) | ||
); | ||
} | ||
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") { | ||
SCRIPT_MAP[scriptURL] = { loaded: true, observers: {} }; | ||
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]; | ||
} | ||
if (SCRIPT_MAP[scriptURL]) { | ||
let entry = SCRIPT_MAP[scriptURL]; | ||
if (entry && (entry.loaded || entry.errored)) { | ||
this.asyncScriptLoaderHandleLoad(entry); | ||
componentDidMount() { | ||
const scriptURL = this.setupScriptURL(); | ||
const key = this.asyncScriptLoaderGetScriptLoaderID(); | ||
const { globalName, callbackName } = options; | ||
// check if global object already attached to window | ||
if (globalName && typeof window[globalName] !== "undefined") { | ||
SCRIPT_MAP[scriptURL] = { loaded: true, observers: {} }; | ||
} | ||
// check if script loading already | ||
if (SCRIPT_MAP[scriptURL]) { | ||
let entry = SCRIPT_MAP[scriptURL]; | ||
// if loaded or errored then "finish" | ||
if (entry && (entry.loaded || entry.errored)) { | ||
this.asyncScriptLoaderHandleLoad(entry); | ||
return; | ||
} | ||
// if still loading then callback to observer queue | ||
entry.observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry); | ||
return; | ||
} | ||
entry.observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry); | ||
return; | ||
} | ||
let observers = {}; | ||
observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry); | ||
SCRIPT_MAP[scriptURL] = { | ||
loaded: false, | ||
observers, | ||
}; | ||
/* | ||
* hasn't started loading | ||
* start the "magic" | ||
* setup script to load and observers | ||
*/ | ||
let observers = {}; | ||
observers[key] = entry => this.asyncScriptLoaderHandleLoad(entry); | ||
SCRIPT_MAP[scriptURL] = { | ||
loaded: false, | ||
observers, | ||
}; | ||
let script = document.createElement("script"); | ||
let script = document.createElement("script"); | ||
script.src = scriptURL; | ||
script.async = true; | ||
script.src = scriptURL; | ||
script.async = true; | ||
let callObserverFuncAndRemoveObserver = func => { | ||
if (SCRIPT_MAP[scriptURL]) { | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
let observersMap = mapEntry.observers; | ||
let callObserverFuncAndRemoveObserver = func => { | ||
if (SCRIPT_MAP[scriptURL]) { | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
let observersMap = mapEntry.observers; | ||
for (let obsKey in observersMap) { | ||
if (func(observersMap[obsKey])) { | ||
delete observersMap[obsKey]; | ||
for (let obsKey in observersMap) { | ||
if (func(observersMap[obsKey])) { | ||
delete observersMap[obsKey]; | ||
} | ||
} | ||
} | ||
}; | ||
if (callbackName && typeof window !== "undefined") { | ||
window[callbackName] = () => | ||
this.asyncScriptLoaderTriggerOnScriptLoaded(); | ||
} | ||
}; | ||
if (callbackName && typeof window !== "undefined") { | ||
window[callbackName] = () => | ||
this.asyncScriptLoaderTriggerOnScriptLoaded(); | ||
script.onload = () => { | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.loaded = true; | ||
callObserverFuncAndRemoveObserver(observer => { | ||
if (callbackName) { | ||
return false; | ||
} | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
}; | ||
script.onerror = event => { | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.errored = true; | ||
callObserverFuncAndRemoveObserver(observer => { | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
}; | ||
document.body.appendChild(script); | ||
} | ||
script.onload = () => { | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.loaded = true; | ||
callObserverFuncAndRemoveObserver(observer => { | ||
if (callbackName) { | ||
return false; | ||
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) { | ||
if (allScripts[i].src.indexOf(scriptURL) > -1) { | ||
if (allScripts[i].parentNode) { | ||
allScripts[i].parentNode.removeChild(allScripts[i]); | ||
} | ||
} | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
} | ||
}; | ||
script.onerror = event => { | ||
// Clean the observer entry | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
mapEntry.errored = true; | ||
callObserverFuncAndRemoveObserver(observer => { | ||
observer(mapEntry); | ||
return true; | ||
}); | ||
} | ||
}; | ||
// (old) MSIE browsers may call "onreadystatechange" instead of "onload" | ||
script.onreadystatechange = () => { | ||
if (this.readyState === "loaded") { | ||
// wait for other events, then call onload if default onload hadn't been called | ||
window.setTimeout(() => { | ||
const mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry && mapEntry.loaded !== true) { | ||
script.onload(); | ||
} | ||
}, 0); | ||
} | ||
}; | ||
document.body.appendChild(script); | ||
} | ||
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) { | ||
if (allScripts[i].src.indexOf(scriptURL) > -1) { | ||
if (allScripts[i].parentNode) { | ||
allScripts[i].parentNode.removeChild(allScripts[i]); | ||
} | ||
delete mapEntry.observers[this.asyncScriptLoaderGetScriptLoaderID()]; | ||
if (options.removeOnUnmount === true) { | ||
delete SCRIPT_MAP[scriptURL]; | ||
} | ||
} | ||
} | ||
// Clean the observer entry | ||
let mapEntry = SCRIPT_MAP[scriptURL]; | ||
if (mapEntry) { | ||
delete mapEntry.observers[this.asyncScriptLoaderGetScriptLoaderID()]; | ||
if (options.removeOnUnmount === true) { | ||
delete SCRIPT_MAP[scriptURL]; | ||
render() { | ||
const globalName = options.globalName; | ||
// remove asyncScriptOnLoad from childProps | ||
let { asyncScriptOnLoad, forwardedRef, ...childProps } = this.props; // eslint-disable-line no-unused-vars | ||
if (globalName && typeof window !== "undefined") { | ||
childProps[globalName] = | ||
typeof window[globalName] !== "undefined" | ||
? window[globalName] | ||
: undefined; | ||
} | ||
childProps.ref = forwardedRef; | ||
return createElement(WrappedComponent, childProps); | ||
} | ||
} | ||
render() { | ||
const globalName = options.globalName; | ||
// remove asyncScriptOnLoad from childprops | ||
let { asyncScriptOnLoad, ...childProps } = this.props; | ||
if (globalName && typeof window !== "undefined") { | ||
childProps[globalName] = | ||
typeof window[globalName] !== "undefined" | ||
? window[globalName] | ||
: undefined; | ||
} | ||
return ( | ||
<Component | ||
ref={comp => { | ||
this.__childComponent = comp; | ||
}} | ||
{...childProps} | ||
/> | ||
); | ||
} | ||
} | ||
AsyncScriptLoader.displayName = `AsyncScriptLoader(${wrappedComponentName})`; | ||
AsyncScriptLoader.propTypes = { | ||
asyncScriptOnLoad: PropTypes.func, | ||
}; | ||
// Note the second param "ref" provided by React.forwardRef. | ||
// We can pass it along to AsyncScriptLoader as a regular prop, e.g. "forwardedRef" | ||
// And it can then be attached to the Component. | ||
const ForwardedComponent = forwardRef((props, ref) => { | ||
return createElement(AsyncScriptLoader, {...props, forwardedRef: ref }); | ||
}); | ||
ForwardedComponent.displayName = `AsyncScriptLoader(${wrappedComponentName})`; | ||
ForwardedComponent.propTypes = { | ||
asyncScriptOnLoad: PropTypes.func, | ||
}; | ||
if (options.exposeFuncs) { | ||
options.exposeFuncs.forEach(funcToExpose => { | ||
AsyncScriptLoader.prototype[funcToExpose] = function() { | ||
return this.getComponent()[funcToExpose](...arguments); | ||
}; | ||
}); | ||
return hoistStatics(ForwardedComponent, WrappedComponent); | ||
} | ||
return AsyncScriptLoader; | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
27330
3
31
375
113
+ Addedhoist-non-react-statics@3.3.2(transitive)
Updatedprop-types@^15.5.0