UNPKG

9.58 kBJavaScriptView Raw
1/* @flow */
2
3import {Component} from 'react';
4import PropTypes from 'prop-types';
5
6import StyleKeeper from './style-keeper';
7import resolveStyles from './resolve-styles';
8import getRadiumStyleState from './get-radium-style-state';
9
10const KEYS_TO_IGNORE_WHEN_COPYING_PROPERTIES = [
11 'arguments',
12 'callee',
13 'caller',
14 'length',
15 'name',
16 'prototype',
17 'type'
18];
19
20let RADIUM_PROTO: Object;
21let RADIUM_METHODS;
22
23function copyProperties(source, target) {
24 Object.getOwnPropertyNames(source).forEach(key => {
25 if (
26 KEYS_TO_IGNORE_WHEN_COPYING_PROPERTIES.indexOf(key) < 0 &&
27 !target.hasOwnProperty(key)
28 ) {
29 const descriptor = Object.getOwnPropertyDescriptor(source, key);
30 Object.defineProperty(target, key, descriptor);
31 }
32 });
33}
34
35// Handle scenarios of:
36// - Inherit from `React.Component` in any fashion
37// See: https://github.com/FormidableLabs/radium/issues/738
38// - There's an explicit `render` field defined
39function isStateless(component: Function): boolean {
40 const proto = component.prototype || {};
41
42 return !component.isReactComponent &&
43 !proto.isReactComponent &&
44 !component.render &&
45 !proto.render;
46}
47
48// Check if value is a real ES class in Native / Node code.
49// See: https://stackoverflow.com/a/30760236
50function isNativeClass(component: Function): boolean {
51 return typeof component === 'function' &&
52 /^\s*class\s+/.test(component.toString());
53}
54
55// Manually apply babel-ish class inheritance.
56function inherits(subClass, superClass) {
57 if (typeof superClass !== 'function' && superClass !== null) {
58 throw new TypeError(
59 `Super expression must either be null or a function, not ${typeof superClass}`
60 );
61 }
62
63 subClass.prototype = Object.create(superClass && superClass.prototype, {
64 constructor: {
65 value: subClass,
66 enumerable: false,
67 writable: true,
68 configurable: true
69 }
70 });
71
72 if (superClass) {
73 if (Object.setPrototypeOf) {
74 Object.setPrototypeOf(subClass, superClass);
75 } else {
76 subClass.__proto__ = superClass; // eslint-disable-line no-proto
77 }
78 }
79}
80
81export default function enhanceWithRadium(
82 configOrComposedComponent: Class<any> | constructor | Function | Object,
83 config?: Object = {}
84): constructor {
85 if (typeof configOrComposedComponent !== 'function') {
86 const newConfig = {...config, ...configOrComposedComponent};
87 return function(configOrComponent) {
88 return enhanceWithRadium(configOrComponent, newConfig);
89 };
90 }
91
92 const component: Function = configOrComposedComponent;
93 let ComposedComponent: constructor = component;
94
95 // Handle Native ES classes.
96 if (isNativeClass(ComposedComponent)) {
97 // Manually approximate babel's class transpilation, but _with_ a real `new` call.
98 ComposedComponent = (function(OrigComponent): constructor {
99 function NewComponent() {
100 // Ordinarily, babel would produce something like:
101 //
102 // ```
103 // return _possibleConstructorReturn(this, OrigComponent.apply(this, arguments));
104 // ```
105 //
106 // Instead, we just call `new` directly without the `_possibleConstructorReturn` wrapper.
107 const source = new OrigComponent(...arguments);
108
109 // Then we manually update context with properties.
110 copyProperties(source, this);
111
112 return this;
113 }
114
115 inherits(NewComponent, OrigComponent);
116
117 return NewComponent;
118 })(ComposedComponent);
119 }
120
121 // Handle stateless components
122 if (isStateless(ComposedComponent)) {
123 ComposedComponent = class extends Component<any, Object> {
124 render() {
125 return component(this.props, this.context);
126 }
127 };
128
129 ComposedComponent.displayName = component.displayName || component.name;
130 }
131
132 // Shallow copy composed if still original (we may mutate later).
133 if (ComposedComponent === component) {
134 ComposedComponent = class extends ComposedComponent {};
135 }
136
137 class RadiumEnhancer extends ComposedComponent {
138 static _isRadiumEnhanced = true;
139
140 state: Object;
141
142 _radiumMediaQueryListenersByQuery: {
143 [query: string]: {remove: () => void}
144 };
145 _radiumMouseUpListener: {remove: () => void};
146 _radiumIsMounted: boolean;
147 _lastRadiumState: Object;
148 _extraRadiumStateKeys: any;
149
150 constructor() {
151 super(...arguments);
152
153 this.state = this.state || {};
154 this.state._radiumStyleState = {};
155 this._radiumIsMounted = true;
156
157 const self: Object = this;
158
159 // Handle es7 arrow functions on React class method names by detecting
160 // and transfering the instance method to original class prototype.
161 // (Using a copy of the class).
162 // See: https://github.com/FormidableLabs/radium/issues/738
163 RADIUM_METHODS.forEach(name => {
164 const thisDesc = Object.getOwnPropertyDescriptor(self, name);
165 const thisMethod = (thisDesc || {}).value;
166
167 // Only care if have instance method.
168 if (!thisMethod) {
169 return;
170 }
171
172 const radiumDesc = Object.getOwnPropertyDescriptor(RADIUM_PROTO, name);
173 const radiumProtoMethod = radiumDesc.value;
174 const superProtoMethod = ComposedComponent.prototype[name];
175
176 // Allow transfer when:
177 // 1. have an instance method
178 // 2. the super class prototype doesn't have any method
179 // 3. it is not already the radium prototype's
180 if (!superProtoMethod && thisMethod !== radiumProtoMethod) {
181 // Transfer dynamic render component to Component prototype (copy).
182 Object.defineProperty(ComposedComponent.prototype, name, thisDesc);
183
184 // Remove instance property, leaving us to have a contrived
185 // inheritance chain of (1) radium, (2) superclass.
186 delete self[name];
187 }
188 });
189 }
190
191 componentWillUnmount() {
192 if (super.componentWillUnmount) {
193 super.componentWillUnmount();
194 }
195
196 this._radiumIsMounted = false;
197
198 if (this._radiumMouseUpListener) {
199 this._radiumMouseUpListener.remove();
200 }
201
202 if (this._radiumMediaQueryListenersByQuery) {
203 Object.keys(this._radiumMediaQueryListenersByQuery).forEach(
204 function(query) {
205 this._radiumMediaQueryListenersByQuery[query].remove();
206 },
207 this
208 );
209 }
210 }
211
212 getChildContext() {
213 const superChildContext = super.getChildContext
214 ? super.getChildContext()
215 : {};
216
217 if (!this.props.radiumConfig) {
218 return superChildContext;
219 }
220
221 const newContext = {...superChildContext};
222
223 if (this.props.radiumConfig) {
224 newContext._radiumConfig = this.props.radiumConfig;
225 }
226
227 return newContext;
228 }
229
230 render() {
231 const renderedElement = super.render();
232 let currentConfig = this.props.radiumConfig ||
233 this.context._radiumConfig ||
234 config;
235
236 if (config && currentConfig !== config) {
237 currentConfig = {
238 ...config,
239 ...currentConfig
240 };
241 }
242
243 const {extraStateKeyMap, element} = resolveStyles(
244 this,
245 renderedElement,
246 currentConfig
247 );
248 this._extraRadiumStateKeys = Object.keys(extraStateKeyMap);
249
250 return element;
251 }
252
253 /* eslint-disable react/no-did-update-set-state, no-unused-vars */
254 componentDidUpdate(prevProps, prevState) {
255 if (super.componentDidUpdate) {
256 super.componentDidUpdate.call(this, prevProps, prevState);
257 }
258
259 if (this._extraRadiumStateKeys.length > 0) {
260 const trimmedRadiumState = this._extraRadiumStateKeys.reduce(
261 (state, key) => {
262 const {[key]: extraStateKey, ...remainingState} = state;
263 return remainingState;
264 },
265 getRadiumStyleState(this)
266 );
267
268 this._lastRadiumState = trimmedRadiumState;
269 this.setState({_radiumStyleState: trimmedRadiumState});
270 }
271 }
272 /* eslint-enable react/no-did-update-set-state, no-unused-vars */
273 }
274
275 // Lazy infer the method names of the Enhancer.
276 RADIUM_PROTO = RadiumEnhancer.prototype;
277 RADIUM_METHODS = Object.getOwnPropertyNames(RADIUM_PROTO).filter(
278 n => n !== 'constructor' && typeof RADIUM_PROTO[n] === 'function'
279 );
280
281 // Class inheritance uses Object.create and because of __proto__ issues
282 // with IE <10 any static properties of the superclass aren't inherited and
283 // so need to be manually populated.
284 // See http://babeljs.io/docs/advanced/caveats/#classes-10-and-below-
285 copyProperties(component, RadiumEnhancer);
286
287 if (process.env.NODE_ENV !== 'production') {
288 // This also fixes React Hot Loader by exposing the original components top
289 // level prototype methods on the Radium enhanced prototype as discussed in
290 // https://github.com/FormidableLabs/radium/issues/219.
291 copyProperties(ComposedComponent.prototype, RadiumEnhancer.prototype);
292 }
293
294 if (RadiumEnhancer.propTypes && RadiumEnhancer.propTypes.style) {
295 RadiumEnhancer.propTypes = {
296 ...RadiumEnhancer.propTypes,
297 style: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
298 };
299 }
300
301 RadiumEnhancer.displayName = component.displayName ||
302 component.name ||
303 'Component';
304
305 RadiumEnhancer.contextTypes = {
306 ...RadiumEnhancer.contextTypes,
307 _radiumConfig: PropTypes.object,
308 _radiumStyleKeeper: PropTypes.instanceOf(StyleKeeper)
309 };
310
311 RadiumEnhancer.childContextTypes = {
312 ...RadiumEnhancer.childContextTypes,
313 _radiumConfig: PropTypes.object,
314 _radiumStyleKeeper: PropTypes.instanceOf(StyleKeeper)
315 };
316
317 return RadiumEnhancer;
318}