Skip to content

Commit 7e9ba10

Browse files
committed
feat: initial commit
1 parent 7a8420e commit 7e9ba10

File tree

6 files changed

+479
-14
lines changed

6 files changed

+479
-14
lines changed

README.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,67 @@
55
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
77

8-
an easier to use dynamic script loader
8+
an easier to use dynamic script loader with a [render prop](https://reactjs.org/docs/render-props.html)
9+
10+
## Version notes
11+
12+
* supports React 15 or 16
13+
* if building for legacy browsers with a bundler like Webpack that supports the
14+
`module` field of `package.json`, you will probably need to add a rule to
15+
transpile this package.
916

1017
## Installation
1118

1219
```sh
1320
npm install --save react-render-props-script-loader
1421
```
22+
23+
## Example
24+
25+
```js
26+
import * as React from 'react'
27+
import ScriptLoader from 'react-render-props-script-loader'
28+
29+
import MapView from './MapView'
30+
31+
export const MapViewContainer = props => (
32+
<ScriptLoader
33+
type="text/javascript"
34+
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"
35+
onLoad={() => console.log('loaded google maps!')}
36+
onError={error => console.error('failed to load google maps:', error.stack)}
37+
>
38+
{({loading, error}) => {
39+
if (loading) return <h3>Loading Google Maps API...</h3>
40+
if (error) return <h3>Failed to load Google Maps API: {error.message}</h3>
41+
return <MapView {...props} />
42+
}}
43+
</ScriptLoader>
44+
)
45+
```
46+
47+
## API
48+
49+
The package exports a single component with the following props:
50+
51+
### `src` (**required** `string`)
52+
53+
The script source.
54+
55+
### `onLoad` (`?() => any`)
56+
57+
A callback that `ScriptLoader` will call once the script has been loaded
58+
59+
### `onError` (`?(error: Error) => any`)
60+
61+
A callback that `ScriptLoader` will call if there was an error loading the
62+
script
63+
64+
### `children` (`?(state: State) => ?React.Node`)
65+
66+
The render function. It will be passed the following props, and may return
67+
the content to display:
68+
69+
* `loading` (`boolean`) - `true` iff the script is loading
70+
* `loaded` (`boolean`) - `true` iff the script successfully loaded
71+
* `error` (`?Error`) - the `Error` that occurred if the script failed to load

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@
7171
"babel-eslint": "^7.1.1",
7272
"babel-plugin-flow-react-proptypes": "^17.0.0",
7373
"babel-plugin-istanbul": "^4.0.0",
74+
"babel-plugin-syntax-dynamic-import": "^6.18.0",
75+
"babel-plugin-transform-class-properties": "^6.24.1",
76+
"babel-plugin-transform-export-extensions": "^6.22.0",
77+
"babel-plugin-transform-object-rest-spread": "^6.26.0",
7478
"babel-plugin-transform-react-constant-elements": "^6.9.1",
7579
"babel-plugin-transform-runtime": "^6.23.0",
7680
"babel-preset-env": "^1.7.0",
7781
"babel-preset-flow": "^6.23.0",
7882
"babel-preset-react": "^6.16.0",
79-
"babel-plugin-syntax-dynamic-import": "^6.18.0",
80-
"babel-plugin-transform-class-properties": "^6.24.1",
81-
"babel-plugin-transform-export-extensions": "^6.22.0",
82-
"babel-plugin-transform-object-rest-spread": "^6.26.0",
8383
"babel-preset-stage-1": "^6.24.1",
8484
"babel-register": "^6.23.0",
8585
"babel-runtime": "^6.23.0",
@@ -105,6 +105,7 @@
105105
"react-dom": "^16.2.0",
106106
"rimraf": "^2.6.0",
107107
"semantic-release": "^15.1.4",
108+
"sinon": "^6.1.5",
108109
"travis-deploy-once": "^4.3.1"
109110
},
110111
"peerDependencies": {
@@ -113,4 +114,4 @@
113114
"dependencies": {
114115
"prop-types": "^15.0.0"
115116
}
116-
}
117+
}

src/index.js

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,65 @@
1-
/* @flow */
1+
// @flow
22

33
import * as React from 'react'
4+
import loadScript, {getState} from './loadScript'
45

5-
const Hello = () => <div>Hello world!</div>
6+
export type State = {
7+
loading: boolean,
8+
loaded: boolean,
9+
error: ?Error,
10+
}
611

7-
export default Hello
12+
export type Props = {
13+
src: string,
14+
onLoad?: ?() => any,
15+
onError?: ?(error: Error) => any,
16+
children?: ?(state: State) => ?React.Node,
17+
}
18+
19+
export default class ScriptLoader extends React.PureComponent<Props, State> {
20+
state = getState(this.props)
21+
promise: ?Promise<void>
22+
23+
load() {
24+
const {props} = this
25+
const {
26+
onLoad, onError,
27+
children, // eslint-disable-line no-unused-vars
28+
...loadProps
29+
} = props
30+
const promise = loadScript(loadProps)
31+
if (this.promise !== promise) {
32+
this.promise = promise
33+
this.setState(getState(props))
34+
promise.then(
35+
() => {
36+
if (this.promise !== promise) return
37+
if (onLoad) onLoad()
38+
this.setState(getState(props))
39+
},
40+
(error: Error) => {
41+
if (this.promise !== promise) return
42+
if (onError) onError(error)
43+
this.setState(getState(props))
44+
}
45+
)
46+
}
47+
}
48+
49+
componentDidMount() {
50+
this.load()
51+
}
52+
53+
componentDidUpdate() {
54+
this.load()
55+
}
56+
57+
componentWillUnmount() {
58+
this.promise = null
59+
}
60+
61+
render(): ?React.Node {
62+
const {children} = this.props
63+
if (children) return children({...this.state})
64+
}
65+
}

src/loadScript.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// @flow
2+
3+
/* eslint-env browser */
4+
5+
type Props = {
6+
src: string,
7+
}
8+
9+
const loadScript = ({src, ...props}: Props): Promise<void> => new Promise(
10+
(resolve: () => void, reject: (error?: Error) => void) => {
11+
if (typeof document === 'undefined') {
12+
reject(new Error('server-side rendering is not supported'))
13+
return
14+
}
15+
if (typeof document.querySelector === 'function') {
16+
if (document.querySelector(`script[src="${encodeURIComponent(src)}"]`)) {
17+
resolve()
18+
return
19+
}
20+
}
21+
const script = document.createElement('script')
22+
script.src = src
23+
Object.keys(props).forEach(key => script.setAttribute(key, props[key]))
24+
script.onload = resolve
25+
script.onerror = reject
26+
if (document.body) document.body.appendChild(script)
27+
}
28+
)
29+
30+
const results: {[src: string]: {error: ?Error}} = {}
31+
const promises: {[src: string]: Promise<any>} = {}
32+
33+
export default (props: Props): Promise<any> =>
34+
promises[props.src] || (promises[props.src] = loadScript(props).then(
35+
() => results[props.src] = {error: null},
36+
(error: any = new Error(`failed to load ${props.src}`)) => {
37+
results[props.src] = {error}
38+
throw error
39+
}
40+
))
41+
42+
export function getState({src}: Props): {loading: boolean, loaded: boolean, error: ?Error} {
43+
const result = results[src]
44+
return {
45+
loading: result == null,
46+
loaded: result ? !result.error : false,
47+
error: result && result.error,
48+
}
49+
}

0 commit comments

Comments
 (0)