Skip to content
This repository was archived by the owner on Feb 18, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/models/ComponentStyle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import hashStr from '../vendor/glamor/hash'
import type { RuleSet, NameGenerator, Flattener, Stringifier } from '../types'
import StyleSheet from './StyleSheet'
import isStyledComponent from '../utils/isStyledComponent'
import getComponentCssSelector from '../utils/getComponentCssSelector'

const isStaticRules = (rules: RuleSet): boolean => {
for (let i = 0; i < rules.length; i += 1) {
Expand Down Expand Up @@ -33,7 +34,6 @@ export default (nameGenerator: NameGenerator, flatten: Flattener, stringifyRules
isStatic: boolean
lastClassName: ?string


constructor(rules: RuleSet, componentId: string) {
this.rules = rules
this.isStatic = isStaticRules(rules)
Expand All @@ -49,7 +49,11 @@ export default (nameGenerator: NameGenerator, flatten: Flattener, stringifyRules
* Hashes it, wraps the whole chunk in a .hash1234 {}
* Returns the hash to be injected on render()
* */
generateAndInjectStyles(executionContext: Object, styleSheet: StyleSheet) {
generateAndInjectStyles(
executionContext: Object,
styleSheet: StyleSheet,
options: Object = {},
) {
const { isStatic, lastClassName } = this
if (isStatic && lastClassName !== undefined) {
return lastClassName
Expand All @@ -74,7 +78,9 @@ export default (nameGenerator: NameGenerator, flatten: Flattener, stringifyRules
return name
}

const css = `\n${stringifyRules(flatCSS, `.${name}`)}`
const selector = getComponentCssSelector(name, options)

const css = `\n${stringifyRules(flatCSS, selector)}`
// NOTE: this can only be set when we inject the class-name.
// For some reason, presumably due to how css is stringifyRules behaves in
// differently between client and server, styles break.
Expand Down
2 changes: 1 addition & 1 deletion src/models/StyleSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class StyleSheet {
deferredInjections: { [string]: string } = {}
componentTags: { [string]: Tag }
// helper for `ComponentStyle` to know when it cache static styles.
// staticly styled-component can not safely cache styles on the server
// statically styled-component can not safely cache styles on the server
// without all `ComponentStyle` instances saving a reference to the
// the styleSheet instance they last rendered with,
// or listening to creation / reset events. otherwise you might create
Expand Down
284 changes: 148 additions & 136 deletions src/models/StyledComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,162 +43,174 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
: componentId
}

class BaseStyledComponent extends Component {
static target: Target
static styledComponentId: string
static attrs: Object
static componentStyle: Object
static warnTooManyClasses: Function

attrs = {}
state = {
theme: null,
generatedClassName: '',
}
unsubscribeId: number = -1

unsubscribeFromContext() {
if (this.unsubscribeId !== -1) {
this.context[CHANNEL_NEXT].unsubscribe(this.unsubscribeId)
const createBaseStyledComponent = (options: Object) => {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff is still really bad. I tried a second time.

class BaseStyledComponent extends Component {
static target: Target
static styledComponentId: string
static attrs: Object
static componentStyle: Object
static warnTooManyClasses: Function

attrs = {}
state = {
theme: null,
generatedClassName: '',
}
}
unsubscribeId: number = -1

buildExecutionContext(theme: any, props: any) {
const { attrs } = this.constructor
const context = { ...props, theme }
if (attrs === undefined) {
return context
unsubscribeFromContext() {
if (this.unsubscribeId !== -1) {
this.context[CHANNEL_NEXT].unsubscribe(this.unsubscribeId)
}
}

this.attrs = Object.keys(attrs).reduce((acc, key) => {
const attr = attrs[key]
// eslint-disable-next-line no-param-reassign
acc[key] = typeof attr === 'function' ? 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.instance

// staticaly 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)
buildExecutionContext(theme: any, props: any) {
const { attrs } = this.constructor
const context = { ...props, theme }
if (attrs === undefined) {
return context
}

if (warnTooManyClasses !== undefined) warnTooManyClasses(className)
this.attrs = Object.keys(attrs).reduce((acc, key) => {
const attr = attrs[key]
// eslint-disable-next-line no-param-reassign
acc[key] = typeof attr === 'function' ? attr(context) : attr
return acc
}, {})

return className
return { ...context, ...this.attrs }
}
}

componentWillMount() {
const { componentStyle } = this.constructor
const styledContext = this.context[CHANNEL_NEXT]

// If this is a staticaly-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 || {}
const generatedClassName = this.generateAndInjectStyles(
theme,
this.props,
)
this.setState({ theme, generatedClassName })
generateAndInjectStyles(theme: any, props: any) {
const { attrs, componentStyle, warnTooManyClasses } = this.constructor
const styleSheet = this.context[CONTEXT_KEY] || StyleSheet.instance

// staticaly 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,
options,
)
} else {
const executionContext = this.buildExecutionContext(theme, props)
const className = componentStyle.generateAndInjectStyles(
executionContext,
styleSheet,
options,
)

if (warnTooManyClasses !== undefined) warnTooManyClasses(className)

return className
}
}
}

componentWillReceiveProps(nextProps: { theme?: Theme, [key: string]: any }) {
// If this is a staticaly-styled component, we don't need to listen to
// props changes to update styles
const { componentStyle } = this.constructor
if (componentStyle.isStatic) {
return
componentWillMount() {
const { componentStyle } = this.constructor
const styledContext = this.context[CHANNEL_NEXT]

// If this is a staticaly-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 || {}
const generatedClassName = this.generateAndInjectStyles(
theme,
this.props,
)
this.setState({ theme, generatedClassName })
}
}

this.setState((oldState) => {
const theme = determineTheme(nextProps, oldState.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
componentWillReceiveProps(nextProps: { theme?: Theme, [key: string]: any }) {
// If this is a staticaly-styled component, we don't need to listen to
// props changes to update styles
const { componentStyle } = this.constructor
if (componentStyle.isStatic) {
return
}

const isTargetTag = isTag(target)
this.setState((oldState) => {
const theme = determineTheme(nextProps, oldState.theme, this.constructor.defaultProps)
const generatedClassName = this.generateAndInjectStyles(theme, nextProps)

const className = [
// eslint-disable-next-line react/prop-types
this.props.className,
styledComponentId,
this.attrs.className,
generatedClassName,
].filter(Boolean).join(' ')

const baseProps = {
...this.attrs,
className,
return { theme, generatedClassName }
})
}

if (isStyledComponent(target)) {
baseProps.innerRef = innerRef
} else {
baseProps.ref = innerRef
componentWillUnmount() {
this.unsubscribeFromContext()
}

const propsForElement = Object
.keys(this.props)
.reduce((acc, propName) => {
// Don't pass through non HTML tags through to HTML elements
// always omit innerRef
if (
propName !== 'innerRef' &&
propName !== 'className' &&
(!isTargetTag || validAttr(propName))
) {
// eslint-disable-next-line no-param-reassign
acc[propName] = this.props[propName]
}
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 = {
...this.attrs,
className,
}

return acc
}, baseProps)
if (isStyledComponent(target)) {
baseProps.innerRef = innerRef
} else {
baseProps.ref = innerRef
}

return createElement(target, propsForElement)
const propsForElement = Object
.keys(this.props)
.reduce((acc, propName) => {
// Don't pass through non HTML tags through to HTML elements
// always omit innerRef
if (
propName !== 'innerRef' &&
propName !== 'className' &&
(!isTargetTag || validAttr(propName))
) {
// eslint-disable-next-line no-param-reassign
acc[propName] = this.props[propName]
}

return acc
}, baseProps)

return createElement(target, propsForElement)
}
}

return BaseStyledComponent
}

const createStyledComponent = (
Expand All @@ -209,7 +221,7 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
const {
displayName = isTag(target) ? `styled.${target}` : `Styled(${getComponentName(target)})`,
componentId = generateId(options.displayName, options.parentComponentId),
ParentComponent = BaseStyledComponent,
ParentComponent = createBaseStyledComponent(options),
rules: extendingRules,
attrs,
} = options
Expand Down
17 changes: 17 additions & 0 deletions src/utils/getComponentCssSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @flow

/**
* Adjusts the css selector for the component's css to increase specificity when needed
*/
export default function getComponentCssSelector(componentName: string, options: Object) {
if (options && options.namespaceClasses) {
let namespaceClass = options.namespaceClasses
if (Array.isArray(options.namespaceClasses)) {
namespaceClass = options.namespaceClasses.join(' .')
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


return `.${namespaceClass} .${componentName}`
}

return `.${componentName}`
}
Loading