kitchensink
Version:
Dispatch's awesome components and style guide
376 lines (319 loc) • 10.4 kB
JavaScript
/* @flow */
import type {Config} from './config';
import appendImportantToEachValue from './append-important-to-each-value';
import cssRuleSetToString from './css-rule-set-to-string';
import getState from './get-state';
import getStateKey from './get-state-key';
import hash from './hash';
import {isNestedStyle, mergeStyles} from './merge-styles';
import Plugins from './plugins/';
import ExecutionEnvironment from 'exenv';
import React from 'react';
const DEFAULT_CONFIG = {
plugins: [
Plugins.mergeStyleArray,
Plugins.checkProps,
Plugins.resolveMediaQueries,
Plugins.resolveInteractionStyles,
Plugins.keyframes,
Plugins.visited,
Plugins.removeNestedStyles,
Plugins.prefix,
Plugins.checkProps
]
};
// Gross
let globalState = {};
// Declare early for recursive helpers.
let resolveStyles = ((null: any): (
component: any, // ReactComponent, flow+eslint complaining
renderedElement: any,
config: Config,
existingKeyMap?: {[key: string]: bool},
shouldCheckBeforeResolve: true
) => any);
const _shouldResolveStyles = function(component) {
return (component.type && !component.type._isRadiumEnhanced);
};
const _resolveChildren = function({
children,
component,
config,
existingKeyMap
}) {
if (!children) {
return children;
}
const childrenType = typeof children;
if (childrenType === 'string' || childrenType === 'number') {
// Don't do anything with a single primitive child
return children;
}
if (childrenType === 'function') {
// Wrap the function, resolving styles on the result
return function() {
const result = children.apply(this, arguments);
if (React.isValidElement(result)) {
return resolveStyles(component, result, config, existingKeyMap, true);
}
return result;
};
}
if (React.Children.count(children) === 1 && children.type) {
// If a React Element is an only child, don't wrap it in an array for
// React.Children.map() for React.Children.only() compatibility.
const onlyChild = React.Children.only(children);
return resolveStyles(component, onlyChild, config, existingKeyMap, true);
}
return React.Children.map(
children,
function(child) {
if (React.isValidElement(child)) {
return resolveStyles(component, child, config, existingKeyMap, true);
}
return child;
}
);
};
// Recurse over props, just like children
const _resolveProps = function({
component,
config,
existingKeyMap,
props
}) {
let newProps = props;
Object.keys(props).forEach(prop => {
// We already recurse over children above
if (prop === 'children') {
return;
}
const propValue = props[prop];
if (React.isValidElement(propValue)) {
newProps = {...newProps};
newProps[prop] = resolveStyles(
component,
propValue,
config,
existingKeyMap,
true
);
}
});
return newProps;
};
const _buildGetKey = function({
componentName,
existingKeyMap,
renderedElement
}) {
// We need a unique key to correlate state changes due to user interaction
// with the rendered element, so we know to apply the proper interactive
// styles.
const originalKey = typeof renderedElement.ref === 'string' ?
renderedElement.ref :
renderedElement.key;
const key = getStateKey(originalKey);
let alreadyGotKey = false;
const getKey = function() {
if (alreadyGotKey) {
return key;
}
alreadyGotKey = true;
if (existingKeyMap[key]) {
let elementName;
if (typeof renderedElement.type === 'string') {
elementName = renderedElement.type;
} else if (renderedElement.type.constructor) {
elementName = renderedElement.type.constructor.displayName ||
renderedElement.type.constructor.name;
}
throw new Error(
'Radium requires each element with interactive styles to have a unique ' +
'key, set using either the ref or key prop. ' +
(originalKey ?
'Key "' + originalKey + '" is a duplicate.' :
'Multiple elements have no key specified.') + ' ' +
'Component: "' + componentName + '". ' +
(elementName ? 'Element: "' + elementName + '".' : '')
);
}
existingKeyMap[key] = true;
return key;
};
return getKey;
};
const _setStyleState = function(component, key, stateKey, value) {
if (!component._radiumIsMounted) {
return;
}
const existing = component._lastRadiumState ||
component.state && component.state._radiumStyleState || {};
const state = { _radiumStyleState: {...existing} };
state._radiumStyleState[key] = {...state._radiumStyleState[key]};
state._radiumStyleState[key][stateKey] = value;
component._lastRadiumState = state._radiumStyleState;
component.setState(state);
};
const _runPlugins = function({
component,
config,
existingKeyMap,
props,
renderedElement
}) {
// Don't run plugins if renderedElement is not a simple ReactDOMElement or has
// no style.
if (
!React.isValidElement(renderedElement) ||
typeof renderedElement.type !== 'string' ||
!props.style
) {
return props;
}
let newProps = props;
const plugins = config.plugins || DEFAULT_CONFIG.plugins;
const componentName = component.constructor.displayName ||
component.constructor.name;
const getKey = _buildGetKey({renderedElement, existingKeyMap, componentName});
const getComponentField = key => component[key];
const getGlobalState = key => globalState[key];
const componentGetState = (stateKey, elementKey) =>
getState(component.state, elementKey || getKey(), stateKey);
const setState = (stateKey, value, elementKey) =>
_setStyleState(component, elementKey || getKey(), stateKey, value);
const addCSS = css => {
const styleKeeper = component._radiumStyleKeeper ||
component.context._radiumStyleKeeper;
if (!styleKeeper) {
if (__isTestModeEnabled) {
return {remove() {}};
}
throw new Error(
'To use plugins requiring `addCSS` (e.g. keyframes, media queries), ' +
'please wrap your application in the StyleRoot component. Component ' +
'name: `' + componentName + '`.',
);
}
return styleKeeper.addCSS(css);
};
let newStyle = props.style;
plugins.forEach(plugin => {
const result = plugin({
ExecutionEnvironment,
addCSS,
appendImportantToEachValue,
componentName,
config,
cssRuleSetToString,
getComponentField,
getGlobalState,
getState: componentGetState,
hash,
mergeStyles,
props: newProps,
setState,
isNestedStyle,
style: newStyle
}) || {};
newStyle = result.style || newStyle;
newProps = result.props && Object.keys(result.props).length ?
{...newProps, ...result.props} :
newProps;
const newComponentFields = result.componentFields || {};
Object.keys(newComponentFields).forEach(fieldName => {
component[fieldName] = newComponentFields[fieldName];
});
const newGlobalState = result.globalState || {};
Object.keys(newGlobalState).forEach(key => {
globalState[key] = newGlobalState[key];
});
});
if (newStyle !== props.style) {
newProps = {...newProps, style: newStyle};
}
return newProps;
};
// Wrapper around React.cloneElement. To avoid processing the same element
// twice, whenever we clone an element add a special prop to make sure we don't
// process this element again.
const _cloneElement = function(renderedElement, newProps, newChildren) {
// Only add flag if this is a normal DOM element
if (typeof renderedElement.type === 'string') {
newProps = {...newProps, 'data-radium': true};
}
return React.cloneElement(renderedElement, newProps, newChildren);
};
//
// The nucleus of Radium. resolveStyles is called on the rendered elements
// before they are returned in render. It iterates over the elements and
// children, rewriting props to add event handlers required to capture user
// interactions (e.g. mouse over). It also replaces the style prop because it
// adds in the various interaction styles (e.g. :hover).
//
resolveStyles = function(
component: any, // ReactComponent, flow+eslint complaining
renderedElement: any, // ReactElement
config: Config = DEFAULT_CONFIG,
existingKeyMap?: {[key: string]: boolean},
shouldCheckBeforeResolve: boolean = false,
): any { // ReactElement
existingKeyMap = existingKeyMap || {};
if (
!renderedElement ||
// Bail if we've already processed this element. This ensures that only the
// owner of an element processes that element, since the owner's render
// function will be called first (which will always be the case, since you
// can't know what else to render until you render the parent component).
(renderedElement.props && renderedElement.props['data-radium']) ||
// Bail if this element is a radium enhanced element, because if it is,
// then it will take care of resolving its own styles.
(shouldCheckBeforeResolve && !_shouldResolveStyles(renderedElement))
) {
return renderedElement;
}
const newChildren = _resolveChildren({
children: renderedElement.props.children,
component,
config,
existingKeyMap
});
let newProps = _resolveProps({
component,
config,
existingKeyMap,
props: renderedElement.props
});
newProps = _runPlugins({
component,
config,
existingKeyMap,
props: newProps,
renderedElement
});
// If nothing changed, don't bother cloning the element. Might be a bit
// wasteful, as we add the sentinal to stop double-processing when we clone.
// Assume benign double-processing is better than unneeded cloning.
if (
newChildren === renderedElement.props.children &&
newProps === renderedElement.props
) {
return renderedElement;
}
return _cloneElement(
renderedElement,
newProps !== renderedElement.props ? newProps : {},
newChildren
);
};
// Only for use by tests
let __isTestModeEnabled = false;
if (process.env.NODE_ENV !== 'production') {
resolveStyles.__clearStateForTests = function() {
globalState = {};
};
resolveStyles.__setTestMode = function(isEnabled) {
__isTestModeEnabled = isEnabled;
};
}
export default resolveStyles;