UNPKG

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