glamorous
Version:
React component styling solved
690 lines (573 loc) • 22.9 kB
JavaScript
(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