// @flow import hoist from 'hoist-non-react-statics' import PropTypes from 'prop-types' import { Component, createElement } from 'react' import { CONTEXT_KEY } from '../constants' import createWarnTooManyClasses from '../utils/createWarnTooManyClasses' import determineTheme from '../utils/determineTheme' import { EMPTY_OBJECT } from '../utils/empties' import escape from '../utils/escape' import generateDisplayName from '../utils/generateDisplayName' import getComponentName from '../utils/getComponentName' import isStyledComponent from '../utils/isStyledComponent' import isTag from '../utils/isTag' import validAttr from '../utils/validAttr' import hasInInheritanceChain from '../utils/hasInInheritanceChain' import once from '../utils/once' import ServerStyleSheet from './ServerStyleSheet' import StyleSheet from './StyleSheet' import { CHANNEL_NEXT, contextShape } from './ThemeProvider' import type { Theme } from './ThemeProvider' import type { RuleSet, Target } from '../types' // HACK for generating all static styles without needing to allocate // an empty execution context every single time... const STATIC_EXECUTION_CONTEXT = {} type BaseState = { theme?: ?Theme, generatedClassName?: string, } const modifiedContextShape = { ...contextShape, [CONTEXT_KEY]: PropTypes.oneOfType([ PropTypes.instanceOf(StyleSheet), PropTypes.instanceOf(ServerStyleSheet), ]), } const identifiers = {} /* We depend on components having unique IDs */ const generateId = ( ComponentStyle: Function, _displayName: string, parentComponentId: string ) => { const displayName = typeof _displayName !== 'string' ? 'sc' : escape(_displayName) /** * This ensures uniqueness if two components happen to share * the same displayName. */ const nr = (identifiers[displayName] || 0) + 1 identifiers[displayName] = nr const componentId = `${displayName}-${ComponentStyle.generateName( displayName + nr )}` return parentComponentId !== undefined ? `${parentComponentId}-${componentId}` : componentId } let warnExtendDeprecated = () => {} if (process.env.NODE_ENV !== 'production') { warnExtendDeprecated = once(() => { // eslint-disable-next-line no-console console.warn( 'Warning: The "extend" API will be removed in the upcoming v4.0 release. Use styled(StyledComponent) instead. You can find more information here: https://github.com/styled-components/styled-components/issues/1546' ) }) } // $FlowFixMe class BaseStyledComponent extends Component<*, BaseState> { static target: Target static styledComponentId: string static attrs: Object static componentStyle: Object static defaultProps: Object static warnTooManyClasses: Function attrs = {} state = { theme: null, generatedClassName: '', } unsubscribeId: number = -1 unsubscribeFromContext() { if (this.unsubscribeId !== -1) { this.context[CHANNEL_NEXT].unsubscribe(this.unsubscribeId) } } buildExecutionContext(theme: any, props: any) { const { attrs } = this.constructor const context = { ...props, theme } if (attrs === undefined) { return context } this.attrs = Object.keys(attrs).reduce((acc, key) => { const attr = attrs[key] // eslint-disable-next-line no-param-reassign acc[key] = typeof attr === 'function' && !hasInInheritanceChain(attr, Component) ? attr(context) : attr return acc }, {}) return { ...context, ...this.attrs } } generateAndInjectStyles(theme: any, props: any) { const { attrs, componentStyle, warnTooManyClasses } = this.constructor const styleSheet = this.context[CONTEXT_KEY] || StyleSheet.master // statically styled-components don't need to build an execution context object, // and shouldn't be increasing the number of class names if (componentStyle.isStatic && attrs === undefined) { return componentStyle.generateAndInjectStyles( STATIC_EXECUTION_CONTEXT, styleSheet ) } else { const executionContext = this.buildExecutionContext(theme, props) const className = componentStyle.generateAndInjectStyles( executionContext, styleSheet ) if ( process.env.NODE_ENV !== 'production' && warnTooManyClasses !== undefined ) { warnTooManyClasses(className) } return className } } componentWillMount() { const { componentStyle } = this.constructor const styledContext = this.context[CHANNEL_NEXT] // If this is a statically-styled component, we don't need to the theme // to generate or build styles. if (componentStyle.isStatic) { const generatedClassName = this.generateAndInjectStyles( STATIC_EXECUTION_CONTEXT, this.props ) this.setState({ generatedClassName }) // If there is a theme in the context, subscribe to the event emitter. This // is necessary due to pure components blocking context updates, this circumvents // that by updating when an event is emitted } else if (styledContext !== undefined) { const { subscribe } = styledContext this.unsubscribeId = subscribe(nextTheme => { // This will be called once immediately const theme = determineTheme( this.props, nextTheme, this.constructor.defaultProps ) const generatedClassName = this.generateAndInjectStyles( theme, this.props ) this.setState({ theme, generatedClassName }) }) } else { // eslint-disable-next-line react/prop-types const theme = this.props.theme || EMPTY_OBJECT const generatedClassName = this.generateAndInjectStyles(theme, this.props) this.setState({ theme, generatedClassName }) } } componentWillReceiveProps(nextProps: { theme?: Theme, [key: string]: any }) { // If this is a statically-styled component, we don't need to listen to // props changes to update styles const { componentStyle } = this.constructor if (componentStyle.isStatic) { return } this.setState(prevState => { const theme = determineTheme( nextProps, prevState.theme, this.constructor.defaultProps ) const generatedClassName = this.generateAndInjectStyles(theme, nextProps) return { theme, generatedClassName } }) } componentWillUnmount() { this.unsubscribeFromContext() } render() { // eslint-disable-next-line react/prop-types const { innerRef } = this.props const { generatedClassName } = this.state const { styledComponentId, target } = this.constructor const isTargetTag = isTag(target) const className = [ // eslint-disable-next-line react/prop-types this.props.className, styledComponentId, this.attrs.className, generatedClassName, ] .filter(Boolean) .join(' ') const baseProps: Object = { ...this.attrs, className, } if (isStyledComponent(target)) { baseProps.innerRef = innerRef } else { baseProps.ref = innerRef } const propsForElement = baseProps let key for (key in this.props) { // Don't pass through non HTML tags through to HTML elements // always omit innerRef if ( key !== 'innerRef' && key !== 'className' && (!isTargetTag || validAttr(key)) ) { propsForElement[key] = key === 'style' && key in this.attrs ? { ...this.attrs[key], ...this.props[key] } : this.props[key] } } return createElement(target, propsForElement) } } export default (ComponentStyle: Function, constructWithOptions: Function) => { const createStyledComponent = ( target: Target, options: Object, rules: RuleSet ) => { const { isClass = !isTag(target), displayName = generateDisplayName(target), componentId = generateId( ComponentStyle, options.displayName, options.parentComponentId ), ParentComponent = BaseStyledComponent, rules: extendingRules, attrs, } = options const styledComponentId = options.displayName && options.componentId ? `${escape(options.displayName)}-${options.componentId}` : options.componentId || componentId const componentStyle = new ComponentStyle( extendingRules === undefined ? rules : extendingRules.concat(rules), attrs, styledComponentId ) class StyledComponent extends ParentComponent { static attrs = attrs static componentStyle = componentStyle static contextTypes = modifiedContextShape static displayName = displayName static styledComponentId = styledComponentId static target = target static withComponent(tag: Target) { const { componentId: previousComponentId, ...optionsToCopy } = options const newComponentId = previousComponentId && `${previousComponentId}-${ isTag(tag) ? tag : escape(getComponentName(tag)) }` const newOptions = { ...optionsToCopy, componentId: newComponentId, ParentComponent: StyledComponent, } return createStyledComponent(tag, newOptions, rules) } static get extend() { const { rules: rulesFromOptions, componentId: parentComponentId, ...optionsToCopy } = options const newRules = rulesFromOptions === undefined ? rules : rulesFromOptions.concat(rules) const newOptions = { ...optionsToCopy, rules: newRules, parentComponentId, ParentComponent: StyledComponent, } warnExtendDeprecated() return constructWithOptions(createStyledComponent, target, newOptions) } } if (process.env.NODE_ENV !== 'production') { StyledComponent.warnTooManyClasses = createWarnTooManyClasses(displayName) } if (isClass) { hoist(StyledComponent, target, { // all SC-specific things should not be hoisted attrs: true, componentStyle: true, displayName: true, extend: true, styledComponentId: true, target: true, warnTooManyClasses: true, withComponent: true, }) } return StyledComponent } return createStyledComponent }