Skip to content

Instantly share code, notes, and snippets.

@nhducit nhducit/GoogleApi.js forked from auser/GoogleApi.js
Created Aug 16, 2016

Embed
What would you like to do?

ScriptCache + React + Google Api

The 3 scripts in here are separated for clarity. They are:

  • ScriptCache.js - The backbone of this method which asynchronously loads JavaScript <script> tags on a page. It will only load a single <script> tag on a page per-script tag declaration. If it's already loaded on a page, it calls the callback from the onLoad event immediately.

Sample usage:

this.scriptCache = cache({
  google: 'https://api.google.com/some/script.js'
});
  • GoogleApi.js is a script tag compiler. Essentially, this utility module builds a Google Script tag link allowing us to describe the pieces of the Google API we want to load inusing a JS object and letting it build the endpoint string.

Sample usage:

GoogleApi({
  apiKey: apiKey,
  libraries: ['places']
});
  • GoogleApiComponent.js - The React wrapper which is responsible for loading a component and passing through the window.google object after it's loaded on the page.

Sample usage:

const Container = React.createClass({
  render: function() {
    return <div>Google</div>;
  }
})
export default GoogleApiComponent({
  apiKey: __GAPI_KEY__
})(Container)
export const GoogleApi = function(opts) {
opts = opts || {}
const apiKey = opts.apiKey;
const libraries = opts.libraries || [];
const client = opts.client;
const URL = 'https://maps.googleapis.com/maps/api/js';
const googleVersion = '3.22';
let script = null;
let google = window.google = null;
let loading = false;
let channel = null;
let language = null;
let region = null;
let onLoadEvents = [];
const url = () => {
let url = URL;
let params = {
key: apiKey,
callback: 'CALLBACK_NAME',
libraries: libraries.join(','),
client: client,
v: googleVersion,
channel: channel,
language: language,
region: region
}
let paramStr = Object.keys(params)
.filter(k => !!params[k])
.map(k => `${k}=${params[k]}`).join('&');
return `${url}?${paramStr}`;
}
return url();
}
export default GoogleApi
import React, { PropTypes as T } from 'react'
import ReactDOM from 'react-dom'
import cache from 'utils/cache'
import GoogleApi from 'utils/GoogleApi'
const defaultMapConfig = {}
export const wrapper = (options) => (WrappedComponent) => {
const apiKey = options.apiKey;
const libraries = options.libraries || ['places'];
class Wrapper extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
loaded: false,
map: null,
google: null
}
}
componentDidMount() {
const refs = this.refs;
this.scriptCache.google.onLoad((err, tag) => {
const maps = window.google.maps;
const props = Object.assign({}, this.props, {
loaded: this.state.loaded
});
const mapRef = refs.map;
const node = ReactDOM.findDOMNode(mapRef);
let center = new maps.LatLng(this.props.lat, this.props.lng)
let mapConfig = Object.assign({}, defaultMapConfig, {
center, zoom: this.props.zoom
})
this.map = new maps.Map(node, mapConfig);
this.setState({
loaded: true,
map: this.map,
google: window.google
})
});
}
componentWillMount() {
this.scriptCache = cache({
google: GoogleApi({
apiKey: apiKey,
libraries: libraries
})
});
}
render() {
const props = Object.assign({}, this.props, {
loaded: this.state.loaded,
map: this.state.map,
google: this.state.google,
mapComponent: this.refs.map
})
return (
<div>
<WrappedComponent {...props} />
<div ref='map' />
</div>
)
}
}
return Wrapper;
}
export default wrapper;
let counter = 0;
let scriptMap = new Map();
export const ScriptCache = (function(global) {
return function ScriptCache (scripts) {
const Cache = {}
Cache._onLoad = function (key) {
return (cb) => {
let stored = scriptMap.get(key);
if (stored) {
stored.promise.then(() => {
stored.error ? cb(stored.error) : cb(null, stored)
})
} else {
// TODO:
}
}
}
Cache._scriptTag = (key, src) => {
if (!scriptMap.has(key)) {
let tag = document.createElement('script');
let promise = new Promise((resolve, reject) => {
let resolved = false,
errored = false,
body = document.getElementsByTagName('body')[0];
tag.type = 'text/javascript';
tag.async = false; // Load in order
const cbName = `loaderCB${counter++}${Date.now()}`;
let cb;
let handleResult = (state) => {
return (evt) => {
let stored = scriptMap.get(key);
if (state === 'loaded') {
stored.resolved = true;
resolve(src);
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = []
} else if (state === 'error') {
stored.errored = true;
// stored.handlers.forEach(h => h.call(null, stored))
// stored.handlers = [];
reject(evt)
}
cleanup();
}
}
const cleanup = () => {
if (global[cbName] && typeof global[cbName] === 'function') {
global[cbName] = null;
}
}
tag.onload = handleResult('loaded');
tag.onerror = handleResult('error')
tag.onreadystatechange = () => {
handleResult(tag.readyState)
}
// Pick off callback, if there is one
if (src.match(/callback=CALLBACK_NAME/)) {
src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`)
cb = window[cbName] = tag.onload;
} else {
tag.addEventListener('load', tag.onload)
}
tag.addEventListener('error', tag.onerror);
tag.src = src;
body.appendChild(tag);
return tag;
});
let initialState = {
loaded: false,
error: false,
promise: promise,
tag
}
scriptMap.set(key, initialState);
}
return scriptMap.get(key);
}
Object.keys(scripts).forEach(function(key) {
const script = scripts[key];
Cache[key] = {
tag: Cache._scriptTag(key, script),
onLoad: Cache._onLoad(key)
}
})
return Cache;
}
})(window)
export default ScriptCache;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.