Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubkottnauer committed May 22, 2016
0 parents commit efa24e2
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
@@ -0,0 +1,4 @@
{
"presets": ["es2015", "react", "stage-0"],
"plugins": ["add-module-exports"]
}
55 changes: 55 additions & 0 deletions .eslintrc
@@ -0,0 +1,55 @@
{
"parser" : "babel-eslint",
"plugins": [
"import"
],
"extends" : ["airbnb"],
"rules": {
// Soften some rules.
"comma-dangle": 0, // Nobody cares about commas.
"default-case": 0, // Required default case is nonsense.
"new-cap": [2, {"capIsNew": false, "newIsCap": true}], // For Record() etc.
"no-floating-decimal": 0, // .5 is just fine.
"no-shadow": 0, // Shadowing is a nice language feature.
// eslint-plugin-import
"import/no-unresolved": [2, {"commonjs": true}],
"import/named": 2,
"import/default": 2,
"import/namespace": 2,
"import/export": 2,
// BB rules soften
"max-len": 0,
"curly": 0, // Do not mess up code with {} for one-line ifs.
"key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "minimum"}], // Enable use of nice block object creation.
"no-use-before-define": 0, // Enable to define styles after using them in component.
"react/jsx-no-bind": 0, // Enable arrow functions in Props definitions.
"react/prefer-stateless-function": 0 // Enable functions with state.
},
"globals": {
"after": false,
"afterEach": false,
"before": false,
"beforeEach": false,
"console": false,
"describe": false,
"it": false,
"module": false,
"process": false,
"require": false,
"window": false
},
"settings": {
"import/ignore": [
"node_modules",
"\\.json$"
],
"import/parser": "babel-eslint",
"import/resolve": {
"extensions": [
".js",
".jsx",
".json"
]
}
}
}
1 change: 1 addition & 0 deletions .gitattributes
@@ -0,0 +1 @@
* text=auto
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
node_modules
*.log
.DS_Store
dist
lib
6 changes: 6 additions & 0 deletions .npmignore
@@ -0,0 +1,6 @@
.DS_Store
*.log
src
test
examples
coverage
21 changes: 21 additions & 0 deletions LICENSE.md
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Blueberry Apps s.r.o.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
60 changes: 60 additions & 0 deletions README.md
@@ -0,0 +1,60 @@
#react-load-script
This package simplifies loading of 3rd party scripts in your React applications.

#Motivation
There are situations when you need to use a 3rd party JS library in your React application (jQuery, D3.js for rendering charts, etc.) but you don't need it everywhere and/or you want to use it only in a response to users actions. In cases like this, preloading the whole library when application starts is an unnecessary and expensive operation which could possibly slow down your application.

Using the `Script` component this package provides you with, you can easily load any 3rd party scripts your applications needs directly in a relevant component and show a placeholder while the script is loading (e.g. a loading animation). As soon as the script is fully loaded, a callback function you'll have passed to `Script` is called (see example below).

#API
The package exports a single component with the following props:

## `onCreate`
Called as soon as the script tag is created.

## `onError` (required)
Called in case of an error with the script.

## `onLoad` (required)
Called when the requested script is fully loaded.

## `url` (required)
URL pointing to the script you want to load.

#Example
You can use the following code to load jQuery in your app:

```jsx
import Script from 'react-load-script'

...

render() {
return (
<Script
url="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"
onCreate={this.handleScriptCreate.bind(this)}
onError={this.handleScriptError.bind(this)}
onLoad={this.handleScriptLoad.bind(this)}
/>
)
}

...

handleScriptCreate() {
this.setState({ scriptLoaded: false })
}

handleScriptError() {
this.setState({ scriptError: true })
}

handleScriptLoad() {
this.setState({ scriptLoaded: true })
}

```

#License
MIT 2016
40 changes: 40 additions & 0 deletions package.json
@@ -0,0 +1,40 @@
{
"name": "react-load-script",
"version": "0.0.2",
"description": "react-load-script enables you to easily create components which depend on third party JS scripts",
"main": "lib/index.js",
"author": "Blueberry",
"repository": {
"type": "git",
"url": "https://github.com/blueberryapps/react-load-script.git"
},
"scripts": {
"build": "npm run build:lib",
"build:lib": "babel src --out-dir lib",
"clean": "rimraf lib"
},
"keywords": [
"react",
"script"
],
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.7.0",
"babel-core": "^6.7.0",
"babel-eslint": "^6.0.0",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-transform-runtime": "^6.6.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"eslint": "^2.9.0",
"eslint-config-airbnb": "^8.0.0",
"eslint-plugin-import": "^1.0.3",
"eslint-plugin-jsx-a11y": "^1.0.4",
"eslint-plugin-react": "^5.0.1",
"rimraf": "^2.4.3"
},
"dependencies": {
"react": ">=0.14.2"
}
}
120 changes: 120 additions & 0 deletions src/index.jsx
@@ -0,0 +1,120 @@
import invariant from 'invariant';
import React, { PropTypes as RPT } from 'react';

export default class Script extends React.Component {

static propTypes = {
onCreate: RPT.func,
onError: RPT.func.isRequired,
onLoad: RPT.func.isRequired,
url: RPT.string.isRequired
};

// A dictionary mapping script URLs to a dictionary mapping
// component key to component for all components that are waiting
// for the script to load.
static scriptObservers = {};

// A dictionary mapping script URL to a boolean value indicating if the script
// has already been loaded.
static loadedScripts = {};

// A dictionary mapping script URL to a boolean value indicating if the script
// has failed to load.
static erroredScripts = {};

// A counter used to generate a unique id for each component that uses
// ScriptLoaderMixin.
static idCount = 0;

constructor(props) {
super(props);
this.scriptLoaderId = `id${this.constructor.idCount++}`; // eslint-disable-line space-unary-ops
}

componentDidMount() {
const { url } = this.props;

if (this.constructor.loadedScripts[url]) {
this.runCallback('onLoad');
return;
}

if (this.constructor.erroredScripts[url]) {
this.runCallback('onError');
return;
}

// If the script is loading, add the component to the script's observers
// and return. Otherwise, initialize the script's observers with the component
// and start loading the script.
if (this.constructor.scriptObservers[url]) {
this.constructor.scriptObservers[url][this.scriptLoaderId] = this.runCallback.bind(this);
return;
}

this.constructor.scriptObservers[url] = { [this.scriptLoaderId]: this.runCallback.bind(this) };

this.createScript();
}

componentWillUnmount() {
const { url } = this.props;
const observers = this.constructor.scriptObservers[url];

// If the component is waiting for the script to load, remove the
// component from the script's observers before unmounting the component.
if (observers)
delete observers[this.scriptLoaderId];
}

createScript() {
const { url } = this.props;
const script = document.createElement('script');

this.runCallback('onCreate', false);

script.src = url;
script.async = 1;

const callObserverFuncAndRemoveObserver = (shouldRemoveObserver) => {
const observers = this.constructor.scriptObservers[url];
Object.keys(observers).forEach(key => {
if (shouldRemoveObserver(observers[key]))
delete this.constructor.scriptObservers[url][this.scriptLoaderId];
});
};
script.onload = () => {
this.constructor.loadedScripts[url] = true;
callObserverFuncAndRemoveObserver(observer => {
observer('onLoad');
return true;
});
};

script.onerror = () => {
this.constructor.erroredScripts[url] = true;
callObserverFuncAndRemoveObserver(observer => {
observer('onError');
return true;
});
};

document.body.appendChild(script);
}

runCallback(type, required = true) {
const callback = this.props[type];

invariant(
!required || typeof callback === 'function',
`Callback ${type} must be function, got "${typeof callback}" instead`
);

return callback && callback();
}

render() {
return null;
}
}

0 comments on commit efa24e2

Please sign in to comment.