Skip to content

Commit 0137a67

Browse files
authored
Merge pull request #178 from rangoo94/feature/accept-functions-as-input
#172 improvement: Allow Google HOC to be initialized from props
2 parents 30a3e07 + 1487854 commit 0137a67

File tree

5 files changed

+159
-17
lines changed

5 files changed

+159
-17
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ export default GoogleApiWrapper({
3232
})(MapContainer)
3333
```
3434

35+
Alternatively, the `GoogleApiWrapper` Higher-Order component can be configured by passing a function that will be called with whe wrapped component's `props` and should returned the configuration object.
36+
37+
```javascript
38+
export default GoogleApiWrapper(
39+
(props) => ({
40+
apiKey: props.apiKey,
41+
language: props.language,
42+
}
43+
))(MapContainer)
44+
```
45+
3546
If you want to add a loading container _other than the default_ loading container, simply pass it in the HOC, like so:
3647

3748
```javascript

dist/GoogleApiComponent.js

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@
7979
}
8080

8181
var defaultMapConfig = {};
82+
83+
var serialize = function serialize(obj) {
84+
return JSON.stringify(obj);
85+
};
86+
var isSame = function isSame(obj1, obj2) {
87+
return obj1 === obj2 || serialize(obj1) === serialize(obj2);
88+
};
89+
8290
var defaultCreateCache = function defaultCreateCache(options) {
8391
options = options || {};
8492
var apiKey = options.apiKey;
@@ -106,31 +114,79 @@
106114
);
107115
};
108116

109-
var wrapper = exports.wrapper = function wrapper(options) {
117+
var wrapper = exports.wrapper = function wrapper(input) {
110118
return function (WrappedComponent) {
111-
var createCache = options.createCache || defaultCreateCache;
112-
113119
var Wrapper = function (_React$Component) {
114120
_inherits(Wrapper, _React$Component);
115121

116122
function Wrapper(props, context) {
117123
_classCallCheck(this, Wrapper);
118124

125+
// Build options from input
119126
var _this = _possibleConstructorReturn(this, (Wrapper.__proto__ || Object.getPrototypeOf(Wrapper)).call(this, props, context));
120127

121-
_this.scriptCache = createCache(options);
122-
_this.scriptCache.google.onLoad(_this.onLoad.bind(_this));
123-
_this.LoadingContainer = options.LoadingContainer || DefaultLoadingContainer;
128+
var options = typeof input === 'function' ? input(props) : input;
129+
130+
// Initialize required Google scripts and other configured options
131+
_this.initialize(options);
124132

125133
_this.state = {
126134
loaded: false,
127135
map: null,
128-
google: null
136+
google: null,
137+
options: options
129138
};
130139
return _this;
131140
}
132141

133142
_createClass(Wrapper, [{
143+
key: 'componentWillReceiveProps',
144+
value: function componentWillReceiveProps(props) {
145+
// Do not update input if it's not dynamic
146+
if (typeof input !== 'function') {
147+
return;
148+
}
149+
150+
// Get options to compare
151+
var prevOptions = this.state.options;
152+
var options = typeof input === 'function' ? input(props) : input;
153+
154+
// Ignore when options are not changed
155+
if (isSame(options, prevOptions)) {
156+
return;
157+
}
158+
159+
// Initialize with new options
160+
this.initialize(options);
161+
162+
// Save new options in component state,
163+
// and remove information about previous API handlers
164+
this.setState({
165+
options: options,
166+
loaded: false,
167+
google: null
168+
});
169+
}
170+
}, {
171+
key: 'initialize',
172+
value: function initialize(options) {
173+
// Avoid race condition: remove previous 'load' listener
174+
if (this.unregisterLoadHandler) {
175+
this.unregisterLoadHandler();
176+
this.unregisterLoadHandler = null;
177+
}
178+
179+
// Load cache factory
180+
var createCache = options.createCache || defaultCreateCache;
181+
182+
// Build script
183+
this.scriptCache = createCache(options);
184+
this.unregisterLoadHandler = this.scriptCache.google.onLoad(this.onLoad.bind(this));
185+
186+
// Store information about loading container
187+
this.LoadingContainer = options.LoadingContainer || DefaultLoadingContainer;
188+
}
189+
}, {
134190
key: 'onLoad',
135191
value: function onLoad(err, tag) {
136192
this._gapi = window.google;

dist/lib/ScriptCache.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,27 @@
2525

2626
Cache._onLoad = function (key) {
2727
return function (cb) {
28+
var registered = true;
29+
30+
function unregister() {
31+
registered = false;
32+
}
33+
2834
var stored = scriptMap.get(key);
35+
2936
if (stored) {
3037
stored.promise.then(function () {
31-
stored.error ? cb(stored.error) : cb(null, stored);
38+
if (registered) {
39+
stored.error ? cb(stored.error) : cb(null, stored);
40+
}
41+
3242
return stored;
3343
});
3444
} else {
3545
// TODO:
3646
}
47+
48+
return unregister;
3749
};
3850
};
3951

src/GoogleApiComponent.js

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {ScriptCache} from './lib/ScriptCache';
55
import GoogleApi from './lib/GoogleApi';
66

77
const defaultMapConfig = {};
8+
9+
const serialize = obj => JSON.stringify(obj);
10+
const isSame = (obj1, obj2) => obj1 === obj2 || serialize(obj1) === serialize(obj2);
11+
812
const defaultCreateCache = options => {
913
options = options || {};
1014
const apiKey = options.apiKey;
@@ -26,25 +30,72 @@ const defaultCreateCache = options => {
2630

2731
const DefaultLoadingContainer = props => <div>Loading...</div>;
2832

29-
export const wrapper = options => WrappedComponent => {
30-
const createCache = options.createCache || defaultCreateCache;
31-
33+
export const wrapper = input => WrappedComponent => {
3234
class Wrapper extends React.Component {
3335
constructor(props, context) {
3436
super(props, context);
3537

36-
this.scriptCache = createCache(options);
37-
this.scriptCache.google.onLoad(this.onLoad.bind(this));
38-
this.LoadingContainer =
39-
options.LoadingContainer || DefaultLoadingContainer;
38+
// Build options from input
39+
const options = typeof input === 'function' ? input(props) : input;
40+
41+
// Initialize required Google scripts and other configured options
42+
this.initialize(options);
4043

4144
this.state = {
4245
loaded: false,
4346
map: null,
44-
google: null
47+
google: null,
48+
options: options
4549
};
4650
}
4751

52+
componentWillReceiveProps(props) {
53+
// Do not update input if it's not dynamic
54+
if (typeof input !== 'function') {
55+
return;
56+
}
57+
58+
// Get options to compare
59+
const prevOptions = this.state.options;
60+
const options = typeof input === 'function' ? input(props) : input;
61+
62+
// Ignore when options are not changed
63+
if (isSame(options, prevOptions)) {
64+
return;
65+
}
66+
67+
// Initialize with new options
68+
this.initialize(options);
69+
70+
// Save new options in component state,
71+
// and remove information about previous API handlers
72+
this.setState({
73+
options: options,
74+
loaded: false,
75+
google: null
76+
});
77+
}
78+
79+
initialize(options) {
80+
// Avoid race condition: remove previous 'load' listener
81+
if (this.unregisterLoadHandler) {
82+
this.unregisterLoadHandler();
83+
this.unregisterLoadHandler = null;
84+
}
85+
86+
// Load cache factory
87+
const createCache = options.createCache || defaultCreateCache;
88+
89+
// Build script
90+
this.scriptCache = createCache(options);
91+
this.unregisterLoadHandler =
92+
this.scriptCache.google.onLoad(this.onLoad.bind(this));
93+
94+
// Store information about loading container
95+
this.LoadingContainer =
96+
options.LoadingContainer || DefaultLoadingContainer;
97+
}
98+
4899
onLoad(err, tag) {
49100
this._gapi = window.google;
50101

src/lib/ScriptCache.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,27 @@ export const ScriptCache = (function(global) {
99

1010
Cache._onLoad = function(key) {
1111
return (cb) => {
12+
let registered = true;
13+
14+
function unregister() {
15+
registered = false;
16+
}
17+
1218
let stored = scriptMap.get(key);
19+
1320
if (stored) {
1421
stored.promise.then(() => {
15-
stored.error ? cb(stored.error) : cb(null, stored)
22+
if (registered) {
23+
stored.error ? cb(stored.error) : cb(null, stored)
24+
}
25+
1626
return stored;
1727
});
1828
} else {
1929
// TODO:
2030
}
31+
32+
return unregister;
2133
}
2234
}
2335

0 commit comments

Comments
 (0)