UNPKG

glamorous

Version:

React component styling solved

690 lines (573 loc) 22.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react'), require('glamor')) : typeof define === 'function' && define.amd ? define(['react', 'glamor'], factory) : (global.glamorous = factory(global.React,global.Glamor)); }(this, (function (React,glamor) { 'use strict'; React = React && React.hasOwnProperty('default') ? React['default'] : React; 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 = (typeof window !== 'undefined' ? window : global).PropTypes; /* 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 createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var defineProperty = function (obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }; 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; }; var toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; 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 _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 _ref2; 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, (_ref2 = ThemedComponent.__proto__ || Object.getPrototypeOf(ThemedComponent)).call.apply(_ref2, [this].concat(args))), _this), _this.warned = noWarn, _this.state = { theme: {} }, _this.setTheme = function (theme) { return _this.setState({ theme: theme }); }, _temp), possibleConstructorReturn(_this, _ret); } createClass(ThemedComponent, [{ key: 'componentWillMount', // eslint-disable-next-line complexity value: function componentWillMount() { if (!this.context[CHANNEL]) { if ('development' !== '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 || {}); } } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (this.props.theme !== nextProps.theme) { this.setTheme(nextProps.theme); } } }, { key: 'componentDidMount', value: function componentDidMount() { if (this.context[CHANNEL] && !this.props.theme) { // subscribe to future theme changes this.subscriptionId = this.context[CHANNEL].subscribe(this.setTheme); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { // cleanup subscription this.subscriptionId && this.context[CHANNEL].unsubscribe(this.subscriptionId); } }, { key: 'render', value: 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); ThemedComponent.propTypes = { theme: _PropTypes.object }; var defaultContextTypes = defineProperty({}, CHANNEL, _PropTypes.object); 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 (name.indexOf('css-') === 0) { var style = buildGlamorSrcFromClassName(name); glamorStyles.push(style); } else { glamorlessClassName.push(name); } }); 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) { return defineProperty({}, 'data-' + className, ''); } function getGlamorClassName$1(_ref2) { var styles = _ref2.styles, props = _ref2.props, cssOverrides = _ref2.cssOverrides, cssProp = _ref2.cssProp, context = _ref2.context, displayName = _ref2.displayName; var _handleStyles = handleStyles([].concat(toConsumableArray(styles), [props.className, cssOverrides, cssProp]), props, context), mappedArgs = _handleStyles.mappedArgs, nonGlamorClassNames = _handleStyles.nonGlamorClassNames; // eslint-disable-next-line max-len var devRules = { label: displayName }; var glamorClassName = glamor.css.apply(undefined, [devRules].concat(toConsumableArray(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]; if (typeof current === 'function') { var result = current(props, context); if (typeof result === 'string') { var _extractGlamorStyles = extractGlamorStyles(result), glamorStyles = _extractGlamorStyles.glamorStyles, glamorlessClassName = _extractGlamorStyles.glamorlessClassName; mappedArgs.push.apply(mappedArgs, toConsumableArray(glamorStyles)); nonGlamorClassNames.push.apply(nonGlamorClassNames, toConsumableArray(glamorlessClassName)); } else { mappedArgs.push(result); } } else if (typeof current === 'string') { var _extractGlamorStyles2 = extractGlamorStyles(current), _glamorStyles = _extractGlamorStyles2.glamorStyles, _glamorlessClassName = _extractGlamorStyles2.glamorlessClassName; mappedArgs.push.apply(mappedArgs, toConsumableArray(_glamorStyles)); nonGlamorClassNames.push.apply(nonGlamorClassNames, toConsumableArray(_glamorlessClassName)); } else if (Array.isArray(current)) { var recursed = handleStyles(current, props, context); mappedArgs.push.apply(mappedArgs, toConsumableArray(recursed.mappedArgs)); nonGlamorClassNames.push.apply(nonGlamorClassNames, toConsumableArray(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$1(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$1({ styles: GlamorousComponent.styles, props: props, cssOverrides: cssOverrides, cssProp: cssProp, context: context, displayName: GlamorousComponent.displayName }) : this.className; return React.createElement(GlamorousComponent.comp, _extends({ ref: props.innerRef }, toForward, { className: this.className })); }, { noWarn: true, createElement: false }); 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.func, glam: _PropTypes.object }; 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 }), _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(toConsumableArray(comp.propsToApply), toConsumableArray(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 || componentsComp, // 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 getDisplayName(comp) { return typeof comp === 'string' ? comp : comp.displayName || comp.name || 'unknown'; } /* eslint no-unused-vars:0 */ function splitProps(_ref) { var cssProp = _ref.css, theme = _ref.theme, className = _ref.className, innerRef = _ref.innerRef, glam = _ref.glam, rest = objectWithoutProperties(_ref, ['css', 'theme', 'className', 'innerRef', 'glam']); return { toForward: rest, cssProp: cssProp }; } var glamorous = createGlamorous$1(splitProps); return glamorous; }))); //# sourceMappingURL=glamorous.umd.tiny.js.map