UNPKG

glamorous

Version:

React component styling solved

604 lines (523 loc) 21.3 kB
import React from 'react'; import { css, styleSheet } from 'glamor'; var CHANNEL = '__glamorous__'; /* istanbul ignore next */ var isPreact = false; var _PropTypes = void 0; /* istanbul ignore next */ if (isPreact) { if (!React.PropTypes) { _PropTypes = function PropTypes() { return _PropTypes; }; ['array', 'bool', 'func', 'number', 'object', 'string', 'symbol', 'any', 'arrayOf', 'element', 'instanceOf', 'node', 'objectOf', 'oneOf', 'oneOfType', 'shape', 'exact'].forEach(function (type) { _PropTypes[type] = _PropTypes; }); } // copied from preact-compat /* eslint-disable no-eq-null, eqeqeq, consistent-return */ if (!React.Children) { var Children = { map: function map(children, fn, ctx) { if (children == null) { return null; } children = Children.toArray(children); if (ctx && ctx !== children) { fn = fn.bind(ctx); } return children.map(fn); }, forEach: function forEach(children, fn, ctx) { if (children == null) { return null; } children = Children.toArray(children); if (ctx && ctx !== children) { fn = fn.bind(ctx); } children.forEach(fn); }, count: function count(children) { return children && children.length || 0; }, only: function only(children) { children = Children.toArray(children); if (children.length !== 1) { throw new Error('Children.only() expects only one child.'); } return children[0]; }, toArray: function toArray(children) { if (children == null) { return []; } return [].concat(children); } }; React.Children = Children; } /* eslint-enable no-eq-null, eqeqeq, consistent-return */ } else if (parseFloat(React.version.slice(0, 4)) >= 15.5) { /* istanbul ignore next */ try { _PropTypes = require('prop-types'); /* istanbul ignore next */ } catch (error) { // ignore } } /* istanbul ignore next */ _PropTypes = _PropTypes || React.PropTypes; /* eslint import/no-mutable-exports:0, import/prefer-default-export:0, react/no-deprecated:0 */ var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var objectWithoutProperties = function (obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; function generateWarningMessage(Comp) { var componentName = Comp.displayName || Comp.name || 'FunctionComponent'; // eslint-disable-next-line max-len return 'glamorous warning: Expected component called "' + componentName + '" which uses withTheme to be within a ThemeProvider but none was found.'; } function withTheme(ComponentToTheme) { var _defaultContextTypes; var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref$noWarn = _ref.noWarn, noWarn = _ref$noWarn === undefined ? false : _ref$noWarn, _ref$createElement = _ref.createElement, createElement = _ref$createElement === undefined ? true : _ref$createElement; var ThemedComponent = function (_React$Component) { inherits(ThemedComponent, _React$Component); function ThemedComponent() { var _temp, _this, _ret; classCallCheck(this, ThemedComponent); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.warned = noWarn, _this.state = { theme: {} }, _this.setTheme = function (theme) { return _this.setState({ theme: theme }); }, _temp), possibleConstructorReturn(_this, _ret); } // eslint-disable-next-line complexity ThemedComponent.prototype.componentWillMount = function componentWillMount() { if (!this.context[CHANNEL]) { if (process.env.NODE_ENV !== 'production' && !this.warned) { this.warned = true; // eslint-disable-next-line no-console console.warn(generateWarningMessage(ComponentToTheme)); } } var theme = this.props.theme; if (this.context[CHANNEL]) { // if a theme is provided via props, // it takes precedence over context this.setTheme(theme ? theme : this.context[CHANNEL].getState()); } else { this.setTheme(theme || {}); } }; ThemedComponent.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { this.setTheme(nextProps.theme); } }; ThemedComponent.prototype.componentDidMount = function componentDidMount() { if (this.context[CHANNEL] && !this.props.theme) { // subscribe to future theme changes this.subscriptionId = this.context[CHANNEL].subscribe(this.setTheme); } }; ThemedComponent.prototype.componentWillUnmount = function componentWillUnmount() { // cleanup subscription this.subscriptionId && this.context[CHANNEL].unsubscribe(this.subscriptionId); }; ThemedComponent.prototype.render = function render() { if (createElement) { return React.createElement(ComponentToTheme, _extends({}, this.props, this.state)); } else { // this allows us to effectively use the GlamorousComponent // as our `render` method without going through lifecycle hooks. // Also allows us to forward the context in the scenario where // a user wants to add more context. // eslint-disable-next-line babel/new-cap return ComponentToTheme.call(this, _extends({}, this.props, this.state), this.context); } }; return ThemedComponent; }(React.Component); process.env.NODE_ENV !== "production" ? ThemedComponent.propTypes = { theme: _PropTypes.object } : void 0; var defaultContextTypes = (_defaultContextTypes = {}, _defaultContextTypes[CHANNEL] = _PropTypes.object, _defaultContextTypes); var userDefinedContextTypes = null; // configure the contextTypes to be settable by the user, // however also retaining the glamorous channel. Object.defineProperty(ThemedComponent, 'contextTypes', { enumerable: true, configurable: true, set: function set$$1(value) { userDefinedContextTypes = value; }, get: function get$$1() { // if the user has provided a contextTypes definition, // merge the default context types with the provided ones. if (userDefinedContextTypes) { return _extends({}, defaultContextTypes, userDefinedContextTypes); } return defaultContextTypes; } }); return ThemedComponent; } /** * This function takes a className string and gets all the * associated glamor styles. It's used to merge glamor styles * from a className to make sure that specificity is not * a problem when passing a className to a component. * @param {String} [className=''] the className string * @return {Object} { glamorStyles, glamorlessClassName } * - glamorStyles is an array of all the glamor styles objects * - glamorlessClassName is the rest of the className string * without the glamor classNames */ function extractGlamorStyles(className) { var glamorlessClassName = []; var glamorStyles = []; className.toString().split(' ').forEach(function (name) { if (styleSheet.registered[name.substring(4)] === undefined) { glamorlessClassName.push(name); } else { var style = buildGlamorSrcFromClassName(name); glamorStyles.push(style); } }); return { glamorlessClassName: glamorlessClassName, glamorStyles: glamorStyles }; } /** Glamor's css function returns an object with the shape * * { * [`data-css-${hash}`]: '', * toString() { return `css-${hash}` } * } * * Whenever glamor's build function encounters an object with * this shape it just pulls the resulting styles from the cache. * * note: the toString method is not needed to qualify the shape **/ function buildGlamorSrcFromClassName(className) { var _ref; return _ref = {}, _ref['data-' + className] = '', _ref; } function getGlamorClassName(_ref2) { var styles = _ref2.styles, props = _ref2.props, cssOverrides = _ref2.cssOverrides, cssProp = _ref2.cssProp, context = _ref2.context, displayName = _ref2.displayName; var _handleStyles = handleStyles([].concat(styles, [props.className, cssOverrides, cssProp]), props, context), mappedArgs = _handleStyles.mappedArgs, nonGlamorClassNames = _handleStyles.nonGlamorClassNames; // eslint-disable-next-line max-len var isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; var devRules = isDev ? { label: displayName } : null; var glamorClassName = css.apply(undefined, [devRules].concat(mappedArgs)).toString(); var extras = nonGlamorClassNames.join(' ').trim(); return (glamorClassName + ' ' + extras).trim(); } // this next function is on a "hot" code-path // so it's pretty complex to make sure it's fast. // eslint-disable-next-line complexity function handleStyles(styles, props, context) { var current = void 0; var mappedArgs = []; var nonGlamorClassNames = []; for (var i = 0; i < styles.length; i++) { current = styles[i]; while (typeof current === 'function') { current = current(props, context); } if (typeof current === 'string') { var _extractGlamorStyles = extractGlamorStyles(current), glamorStyles = _extractGlamorStyles.glamorStyles, glamorlessClassName = _extractGlamorStyles.glamorlessClassName; mappedArgs.push.apply(mappedArgs, glamorStyles); nonGlamorClassNames.push.apply(nonGlamorClassNames, glamorlessClassName); } else if (Array.isArray(current)) { var recursed = handleStyles(current, props, context); mappedArgs.push.apply(mappedArgs, recursed.mappedArgs); nonGlamorClassNames.push.apply(nonGlamorClassNames, recursed.nonGlamorClassNames); } else { mappedArgs.push(current); } } return { mappedArgs: mappedArgs, nonGlamorClassNames: nonGlamorClassNames }; } /* * This is a relatively small abstraction that's ripe for open sourcing. * Documentation is in the README.md */ function createGlamorous(splitProps) { return glamorous; /** * This is the main export and the function that people * interact with most directly. * * It accepts a component which can be a string or * a React Component and returns * a "glamorousComponentFactory" * @param {String|ReactComponent} comp the component to render * @param {Object} options helpful info for the GlamorousComponents * @return {Function} the glamorousComponentFactory */ function glamorous(comp) { var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var rootEl = config.rootEl, displayName = config.displayName, shouldClassNameUpdate = config.shouldClassNameUpdate, _config$filterProps = config.filterProps, filterProps = _config$filterProps === undefined ? [] : _config$filterProps, _config$forwardProps = config.forwardProps, forwardProps = _config$forwardProps === undefined ? [] : _config$forwardProps, _config$propsAreCssOv = config.propsAreCssOverrides, propsAreCssOverrides = _config$propsAreCssOv === undefined ? comp.propsAreCssOverrides : _config$propsAreCssOv, basePropsToApply = config.withProps; Object.assign(glamorousComponentFactory, { withConfig: withConfig }); return glamorousComponentFactory; function withConfig(newConfig) { return glamorous(comp, _extends({}, config, newConfig)); } /** * This returns a React Component that renders the comp (closure) * with a className based on the given glamor styles object(s) * @param {...Object|Function} styles the styles to create with glamor. * If any of these are functions, they are invoked with the component * props and the return value is used. * @return {ReactComponent} the ReactComponent function */ function glamorousComponentFactory() { for (var _len = arguments.length, styles = Array(_len), _key = 0; _key < _len; _key++) { styles[_key] = arguments[_key]; } /** * This is a component which will render the comp (closure) * with the glamorous styles (closure). Forwards any valid * props to the underlying component. */ var GlamorousComponent = withTheme(function (props, context) { props = getPropsToApply(GlamorousComponent.propsToApply, {}, props, context); var updateClassName = shouldUpdate(props, context, this.previous); if (shouldClassNameUpdate) { this.previous = { props: props, context: context }; } var _splitProps = splitProps(props, GlamorousComponent), toForward = _splitProps.toForward, cssOverrides = _splitProps.cssOverrides, cssProp = _splitProps.cssProp; // create className to apply this.className = updateClassName ? getGlamorClassName({ styles: GlamorousComponent.styles, props: props, cssOverrides: cssOverrides, cssProp: cssProp, context: context, displayName: GlamorousComponent.displayName }) : this.className; return React.createElement(GlamorousComponent.comp, _extends({ // if innerRef is forwarded we don't want to apply it here ref: 'innerRef' in toForward ? undefined : props.innerRef }, toForward, { className: this.className })); }, { noWarn: true, createElement: false }); process.env.NODE_ENV !== "production" ? GlamorousComponent.propTypes = { // className accepts an object due to glamor's css function // returning an object with a toString method that gives the className className: _PropTypes.oneOfType([_PropTypes.string, _PropTypes.object]), cssOverrides: _PropTypes.object, innerRef: _PropTypes.oneOfType([_PropTypes.func, _PropTypes.object]), glam: _PropTypes.object } : void 0; function shouldUpdate(props, context, previous) { // exiting early so components which do not use this // optimization are not penalized by hanging onto // references to previous props and context if (!shouldClassNameUpdate) { return true; } var update = true; if (previous) { if (!shouldClassNameUpdate(previous.props, props, previous.context, context)) { update = false; } } return update; } Object.assign(GlamorousComponent, getGlamorousComponentMetadata({ comp: comp, styles: styles, rootEl: rootEl, filterProps: filterProps, forwardProps: forwardProps, displayName: displayName, propsToApply: basePropsToApply }), { isGlamorousComponent: true, propsAreCssOverrides: propsAreCssOverrides, withComponent: function (newComp) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var fwp = GlamorousComponent.forwardProps, flp = GlamorousComponent.filterProps, componentProperties = objectWithoutProperties(GlamorousComponent, ['forwardProps', 'filterProps']); return glamorous(_extends({}, componentProperties, { comp: newComp, rootEl: getRootEl(newComp) }), _extends({ // allows the forwardProps and filterProps to be overridden forwardProps: fwp, filterProps: flp }, options))(); }, withProps: function () { for (var _len2 = arguments.length, propsToApply = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { propsToApply[_key2] = arguments[_key2]; } return glamorous(GlamorousComponent, { withProps: propsToApply })(); }, withConfig: withConfig }); return GlamorousComponent; } } function getGlamorousComponentMetadata(_ref) { var comp = _ref.comp, styles = _ref.styles, rootEl = _ref.rootEl, filterProps = _ref.filterProps, forwardProps = _ref.forwardProps, displayName = _ref.displayName, basePropsToApply = _ref.propsToApply; var componentsComp = comp.comp ? comp.comp : comp; var propsToApply = comp.propsToApply ? [].concat(comp.propsToApply, arrayify(basePropsToApply)) : arrayify(basePropsToApply); return { // join styles together (for anyone doing: glamorous(glamorous.a({}), {})) styles: when(comp.styles, styles), // keep track of the ultimate rootEl to render (we never // actually render anything but // the base component, even when people wrap a glamorous // component in glamorous comp: componentsComp, rootEl: rootEl || getRootEl(comp), // join forwardProps and filterProps // (for anyone doing: glamorous(glamorous.a({}), {})) forwardProps: when(comp.forwardProps, forwardProps), filterProps: when(comp.filterProps, filterProps), // set the displayName to something that's slightly more // helpful than `GlamorousComponent` :) displayName: displayName || 'glamorous(' + getDisplayName(comp) + ')', // these are props that should be applied to the component at render time propsToApply: propsToApply }; } } /** * reduces the propsToApply given to a single props object * @param {Array} propsToApply an array of propsToApply objects: * - object * - array of propsToApply items * - function that accepts the accumulated props and the context * @param {Object} accumulator an object to apply props onto * @param {Object} props the props that should ultimately take precedence * @param {*} context the context object * @return {Object} the reduced props */ function getPropsToApply(propsToApply, accumulator, props, context) { // using forEach rather than reduce here because the reduce solution // effectively did the same thing because we manipulate the `accumulator` propsToApply.forEach(function (propsToApplyItem) { if (typeof propsToApplyItem === 'function') { return Object.assign(accumulator, propsToApplyItem(Object.assign({}, accumulator, props), context)); } else if (Array.isArray(propsToApplyItem)) { return Object.assign(accumulator, getPropsToApply(propsToApplyItem, accumulator, props, context)); } return Object.assign(accumulator, propsToApplyItem); }); // props wins return Object.assign(accumulator, props); } function arrayify() { var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return Array.isArray(x) ? x : [x]; } function when(comp, prop) { return comp ? comp.concat(prop) : prop; } function getRootEl(comp) { return comp.rootEl ? comp.rootEl : comp.comp || comp; } function getDisplayName(comp) { return typeof comp === 'string' ? comp : comp.displayName || comp.name || 'unknown'; } /* eslint no-unused-vars:0 */ function splitProps(_ref, _ref2) { var forwardProps = _ref2.forwardProps; var cssProp = _ref.css, innerRef = _ref.innerRef, theme = _ref.theme, className = _ref.className, glam = _ref.glam, rest = objectWithoutProperties(_ref, ['css', 'innerRef', 'theme', 'className', 'glam']); // forward innerRef if user wishes to do so if (innerRef !== undefined && forwardProps.indexOf('innerRef') !== -1) { rest.innerRef = innerRef; } return { toForward: rest, cssProp: cssProp }; } var glamorous = createGlamorous(splitProps); export default glamorous;