UNPKG

10.5 kBJavaScriptView Raw
1/* @flow */
2
3import type {Config} from './config';
4
5import appendImportantToEachValue from './append-important-to-each-value';
6import cssRuleSetToString from './css-rule-set-to-string';
7import getState from './get-state';
8import getStateKey from './get-state-key';
9import hash from './hash';
10import {isNestedStyle, mergeStyles} from './merge-styles';
11import Plugins from './plugins/';
12
13import ExecutionEnvironment from 'exenv';
14import React from 'react';
15
16const DEFAULT_CONFIG = {
17 plugins: [
18 Plugins.mergeStyleArray,
19 Plugins.checkProps,
20 Plugins.resolveMediaQueries,
21 Plugins.resolveInteractionStyles,
22 Plugins.keyframes,
23 Plugins.visited,
24 Plugins.removeNestedStyles,
25 Plugins.prefix,
26 Plugins.checkProps,
27 ],
28};
29
30// Gross
31let globalState = {};
32
33// Declare early for recursive helpers.
34let resolveStyles = ((null: any): (
35 component: any, // ReactComponent, flow+eslint complaining
36 renderedElement: any,
37 config: Config,
38 existingKeyMap?: {[key: string]: boolean},
39 shouldCheckBeforeResolve: true,
40) => any);
41
42const _shouldResolveStyles = function(component) {
43 return component.type && !component.type._isRadiumEnhanced;
44};
45
46const _resolveChildren = function(
47 {
48 children,
49 component,
50 config,
51 existingKeyMap,
52 },
53) {
54 if (!children) {
55 return children;
56 }
57
58 const childrenType = typeof children;
59
60 if (childrenType === 'string' || childrenType === 'number') {
61 // Don't do anything with a single primitive child
62 return children;
63 }
64
65 if (childrenType === 'function') {
66 // Wrap the function, resolving styles on the result
67 return function() {
68 const result = children.apply(this, arguments);
69 if (React.isValidElement(result)) {
70 return resolveStyles(component, result, config, existingKeyMap, true);
71 }
72 return result;
73 };
74 }
75
76 if (React.Children.count(children) === 1 && children.type) {
77 // If a React Element is an only child, don't wrap it in an array for
78 // React.Children.map() for React.Children.only() compatibility.
79 const onlyChild = React.Children.only(children);
80 return resolveStyles(component, onlyChild, config, existingKeyMap, true);
81 }
82
83 return React.Children.map(children, function(child) {
84 if (React.isValidElement(child)) {
85 return resolveStyles(component, child, config, existingKeyMap, true);
86 }
87
88 return child;
89 });
90};
91
92// Recurse over props, just like children
93const _resolveProps = function(
94 {
95 component,
96 config,
97 existingKeyMap,
98 props,
99 },
100) {
101 let newProps = props;
102
103 Object.keys(props).forEach(prop => {
104 // We already recurse over children above
105 if (prop === 'children') {
106 return;
107 }
108
109 const propValue = props[prop];
110 if (React.isValidElement(propValue)) {
111 newProps = {...newProps};
112 newProps[prop] = resolveStyles(
113 component,
114 propValue,
115 config,
116 existingKeyMap,
117 true,
118 );
119 }
120 });
121
122 return newProps;
123};
124
125const _buildGetKey = function(
126 {
127 componentName,
128 existingKeyMap,
129 renderedElement,
130 },
131) {
132 // We need a unique key to correlate state changes due to user interaction
133 // with the rendered element, so we know to apply the proper interactive
134 // styles.
135 const originalKey = typeof renderedElement.ref === 'string'
136 ? renderedElement.ref
137 : renderedElement.key;
138 const key = getStateKey(originalKey);
139
140 let alreadyGotKey = false;
141 const getKey = function() {
142 if (alreadyGotKey) {
143 return key;
144 }
145
146 alreadyGotKey = true;
147
148 if (existingKeyMap[key]) {
149 let elementName;
150 if (typeof renderedElement.type === 'string') {
151 elementName = renderedElement.type;
152 } else if (renderedElement.type.constructor) {
153 elementName = renderedElement.type.constructor.displayName ||
154 renderedElement.type.constructor.name;
155 }
156
157 throw new Error(
158 'Radium requires each element with interactive styles to have a unique ' +
159 'key, set using either the ref or key prop. ' +
160 (originalKey
161 ? 'Key "' + originalKey + '" is a duplicate.'
162 : 'Multiple elements have no key specified.') +
163 ' ' +
164 'Component: "' +
165 componentName +
166 '". ' +
167 (elementName ? 'Element: "' + elementName + '".' : ''),
168 );
169 }
170
171 existingKeyMap[key] = true;
172
173 return key;
174 };
175
176 return getKey;
177};
178
179const _setStyleState = function(component, key, stateKey, value) {
180 if (!component._radiumIsMounted) {
181 return;
182 }
183
184 const existing = component._lastRadiumState ||
185 (component.state && component.state._radiumStyleState) || {};
186
187 const state = {_radiumStyleState: {...existing}};
188 state._radiumStyleState[key] = {...state._radiumStyleState[key]};
189 state._radiumStyleState[key][stateKey] = value;
190
191 component._lastRadiumState = state._radiumStyleState;
192 component.setState(state);
193};
194
195const _runPlugins = function(
196 {
197 component,
198 config,
199 existingKeyMap,
200 props,
201 renderedElement,
202 },
203) {
204 // Don't run plugins if renderedElement is not a simple ReactDOMElement or has
205 // no style.
206 if (
207 !React.isValidElement(renderedElement) ||
208 typeof renderedElement.type !== 'string' ||
209 !props.style
210 ) {
211 return props;
212 }
213
214 let newProps = props;
215
216 const plugins = config.plugins || DEFAULT_CONFIG.plugins;
217
218 const componentName = component.constructor.displayName ||
219 component.constructor.name;
220 const getKey = _buildGetKey({
221 renderedElement,
222 existingKeyMap,
223 componentName,
224 });
225 const getComponentField = key => component[key];
226 const getGlobalState = key => globalState[key];
227 const componentGetState = (stateKey, elementKey) =>
228 getState(component.state, elementKey || getKey(), stateKey);
229 const setState = (stateKey, value, elementKey) =>
230 _setStyleState(component, elementKey || getKey(), stateKey, value);
231
232 const addCSS = css => {
233 const styleKeeper = component._radiumStyleKeeper ||
234 component.context._radiumStyleKeeper;
235 if (!styleKeeper) {
236 if (__isTestModeEnabled) {
237 return {remove() {}};
238 }
239
240 throw new Error(
241 'To use plugins requiring `addCSS` (e.g. keyframes, media queries), ' +
242 'please wrap your application in the StyleRoot component. Component ' +
243 'name: `' +
244 componentName +
245 '`.',
246 );
247 }
248
249 return styleKeeper.addCSS(css);
250 };
251
252 let newStyle = props.style;
253
254 plugins.forEach(plugin => {
255 const result = plugin({
256 ExecutionEnvironment,
257 addCSS,
258 appendImportantToEachValue,
259 componentName,
260 config,
261 cssRuleSetToString,
262 getComponentField,
263 getGlobalState,
264 getState: componentGetState,
265 hash,
266 mergeStyles,
267 props: newProps,
268 setState,
269 isNestedStyle,
270 style: newStyle,
271 }) || {};
272
273 newStyle = result.style || newStyle;
274
275 newProps = result.props && Object.keys(result.props).length
276 ? {...newProps, ...result.props}
277 : newProps;
278
279 const newComponentFields = result.componentFields || {};
280 Object.keys(newComponentFields).forEach(fieldName => {
281 component[fieldName] = newComponentFields[fieldName];
282 });
283
284 const newGlobalState = result.globalState || {};
285 Object.keys(newGlobalState).forEach(key => {
286 globalState[key] = newGlobalState[key];
287 });
288 });
289
290 if (newStyle !== props.style) {
291 newProps = {...newProps, style: newStyle};
292 }
293
294 return newProps;
295};
296
297// Wrapper around React.cloneElement. To avoid processing the same element
298// twice, whenever we clone an element add a special prop to make sure we don't
299// process this element again.
300const _cloneElement = function(renderedElement, newProps, newChildren) {
301 // Only add flag if this is a normal DOM element
302 if (typeof renderedElement.type === 'string') {
303 newProps = {...newProps, 'data-radium': true};
304 }
305
306 return React.cloneElement(renderedElement, newProps, newChildren);
307};
308
309//
310// The nucleus of Radium. resolveStyles is called on the rendered elements
311// before they are returned in render. It iterates over the elements and
312// children, rewriting props to add event handlers required to capture user
313// interactions (e.g. mouse over). It also replaces the style prop because it
314// adds in the various interaction styles (e.g. :hover).
315//
316resolveStyles = function(
317 component: any, // ReactComponent, flow+eslint complaining
318 renderedElement: any, // ReactElement
319 config: Config = DEFAULT_CONFIG,
320 existingKeyMap?: {[key: string]: boolean},
321 shouldCheckBeforeResolve: boolean = false,
322): any {
323 // ReactElement
324 existingKeyMap = existingKeyMap || {};
325 if (
326 !renderedElement ||
327 // Bail if we've already processed this element. This ensures that only the
328 // owner of an element processes that element, since the owner's render
329 // function will be called first (which will always be the case, since you
330 // can't know what else to render until you render the parent component).
331 (renderedElement.props && renderedElement.props['data-radium']) ||
332 // Bail if this element is a radium enhanced element, because if it is,
333 // then it will take care of resolving its own styles.
334 (shouldCheckBeforeResolve && !_shouldResolveStyles(renderedElement))
335 ) {
336 return renderedElement;
337 }
338
339 const newChildren = _resolveChildren({
340 children: renderedElement.props.children,
341 component,
342 config,
343 existingKeyMap,
344 });
345
346 let newProps = _resolveProps({
347 component,
348 config,
349 existingKeyMap,
350 props: renderedElement.props,
351 });
352
353 newProps = _runPlugins({
354 component,
355 config,
356 existingKeyMap,
357 props: newProps,
358 renderedElement,
359 });
360
361 // If nothing changed, don't bother cloning the element. Might be a bit
362 // wasteful, as we add the sentinal to stop double-processing when we clone.
363 // Assume benign double-processing is better than unneeded cloning.
364 if (
365 newChildren === renderedElement.props.children &&
366 newProps === renderedElement.props
367 ) {
368 return renderedElement;
369 }
370
371 return _cloneElement(
372 renderedElement,
373 newProps !== renderedElement.props ? newProps : {},
374 newChildren,
375 );
376};
377
378// Only for use by tests
379let __isTestModeEnabled = false;
380if (process.env.NODE_ENV !== 'production') {
381 resolveStyles.__clearStateForTests = function() {
382 globalState = {};
383 };
384 resolveStyles.__setTestMode = function(isEnabled) {
385 __isTestModeEnabled = isEnabled;
386 };
387}
388
389export default resolveStyles;