-
Notifications
You must be signed in to change notification settings - Fork 100
Extending Styles #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Extending Styles #210
Changes from all commits
dd30606
9ed0c4c
a12cd13
4fcfe3f
9a4c5fc
3785a37
1a3df21
4c3cff4
9e229a8
ca5b83a
c48e84f
065da86
5b0e7cf
0dc71f4
8a185d0
80a9403
97564c3
3fd0427
a364bd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -246,6 +246,42 @@ export default withStyles(({ color, unit }) => ({ | |
|
|
||
| Some components depend on previous styles to be ready in the component tree when mounting (e.g. dimension calculations). Some interfaces add styles to the page asynchronously, which is an obstacle for this. So, we provide the option of flushing the buffered styles before the rendering cycle begins. It is up to the interface to define what this means. | ||
|
|
||
| #### `extendableStyles` (default: `{}`) | ||
|
|
||
| By default, components created using `withStyles()` will not be extendable. To extend a component's style or styles, you must define the paths and predicates that dictate which styles can be extended and with what values. This is useful if your component wants to restrict some styles, while allowing consumers of the component to have flexibility around others. See the `extendStyles()` section for more info on how to extend styles. | ||
|
|
||
| ```jsx | ||
| import React from 'react'; | ||
| import { css, withStyles } from './withStyles'; | ||
|
|
||
| function MyComponent({ withStylesStyles }) { | ||
| return ( | ||
| <div {...css(withStylesStyles.container)}> | ||
| Try to be a rainbow in someone's cloud. | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default withStyles( | ||
| ({ color, unit }) => ({ | ||
| container: { | ||
| color: color.primary, | ||
| background: color.secondary, | ||
| marginBottom: 2 * unit, | ||
| }, | ||
| }), | ||
| { | ||
| extendableStyles: { | ||
| container: { | ||
| color: (value, theme) => true, | ||
| background: (value, theme) => value === theme.color.primary || value === theme.color.secondary | ||
| }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be really great to have an eslint rule that ensures that the shape of the https://github.com/airbnb/eslint-plugin-react-with-styles There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a rule for the above. |
||
| }, | ||
| }, | ||
| )(MyComponent); | ||
| ``` | ||
|
|
||
| The `container.color` predicate allows for any value to be passed in. The `container.background` predicate only allows for the primary and secondary color to be passed. | ||
|
|
||
| ## `css(...styles)` | ||
|
|
||
|
|
@@ -287,6 +323,57 @@ export default withStyles(({ color, unit }) => ({ | |
|
|
||
| `className` and `style` props must not be used on the same elements as `css()`. | ||
|
|
||
| ## `extendStyles()` | ||
|
|
||
| Components that define an "extendableStyles" option allow consumers to extend certain styles via the `extendStyles()` static property. This function takes in a styles thunk, exactly like `withStyles()`, and should return the extended styles. Any styles that are not explicitly defined in the "extendableStyles" option or do not pass the style's predicate function will throw an error. | ||
|
|
||
| ```jsx | ||
| import React from 'react'; | ||
| import { css, withStyles } from './withStyles'; | ||
|
|
||
| function MyComponent({ withStylesStyles }) { | ||
| return ( | ||
| <div {...css(withStylesStyles.container)}> | ||
| Try to be a rainbow in someone's cloud. | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const BaseMyComponent = withStyles( | ||
| ({ color, unit }) => ({ | ||
| container: { | ||
| color: color.primary, | ||
| background: color.secondary, | ||
| marginBottom: 2 * unit, | ||
| }, | ||
| }), | ||
| { | ||
| extendableStyles: { | ||
| container: { | ||
| color: (value, theme) => true, | ||
| background: (value, { color }) => value === color.primary || color.secondary, | ||
| }, | ||
| }, | ||
| }, | ||
| )(MyComponent); | ||
|
|
||
| const ExtendedMyComponent = BaseMyComponent.extendStyles(({ color }) => ({ | ||
| container: { | ||
| color: color.secondary, | ||
| background: color.primary, | ||
| }, | ||
| })); | ||
| ``` | ||
|
|
||
| You can extend a Component that already extending another Component. All validation will still occur. | ||
| ```jsx | ||
| const NestedExtendedMyComponent = ExtendedMyComponent.extendStyles(() => ({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know that TypeScript currently has some troubles with HOCs, but it looks like it might be getting better soon (microsoft/TypeScript#30215). Have you given any thought into how this might work with TypeScript? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah! I think TS's ability to autocomplete and validate arguments will be super nice with |
||
| container: { | ||
| color: 'red', | ||
| }, | ||
| })); | ||
| ``` | ||
|
|
||
| ## Examples | ||
| ### With React Router's `Link` | ||
| [React Router][react-router]'s [`<Link/>`][react-router-link] and [`<IndexLink/>`][react-router-index-link] components accept `activeClassName='...'` and `activeStyle={{...}}` as props. As previously stated, `css(...styles)` must spread to JSX, so simply passing `styles.thing` or even `css(styles.thing)` directly will not work. In order to mimic `activeClassName`/`activeStyles` you can use React Router's [`withRouter()`][react-router-with-router] Higher Order Component to pass `router` as prop to your component and toggle styles based on [`router.isActive(pathOrLoc, indexOnly)`](react-router-is-active). This works because `<Link />` passes down the generated `className` from `css(..styles)` down through to the final leaf. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /* eslint react/forbid-foreign-prop-types: off */ | ||
|
|
||
| import deepmerge from 'deepmerge'; | ||
|
|
||
| // Recursively iterate through the style object, validating the same path exists in the | ||
| // extendableStyles object. | ||
| function validateStyle(style, extendableStyles, theme, path = '') { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| // Stop recursively validating when we hit a style's value and validate the value passes the | ||
| // style's predicate | ||
| if (!style || Array.isArray(style) || typeof style !== 'object') { | ||
| const stylePredicate = extendableStyles; | ||
| if (typeof stylePredicate !== 'function') { | ||
| throw new Error(`withStyles() style predicate should be a function: "${path}". Check the component's "extendableStyles" option.`); | ||
| } | ||
|
|
||
| const isValid = stylePredicate(style, theme); | ||
| if (!isValid) { | ||
| throw new Error(`withStyles() style did not pass the predicate: "${path}": ${style}. Check the component's "extendableStyles" option.`); | ||
| } | ||
|
|
||
| return; | ||
TaeKimJR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| const styleKeys = Object.keys(style); | ||
| if (styleKeys.length > 0) { | ||
| styleKeys.forEach((styleKey) => { | ||
| const currentPath = `${path}.${styleKey}`; | ||
| const isValid = extendableStyles[styleKey]; | ||
ljharb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!isValid) { | ||
| throw new Error( | ||
| `withStyles() extending style is invalid: "${currentPath}". If this style is expected, add it to the component's "extendableStyles" option.`, | ||
| ); | ||
| } | ||
| validateStyle(style[styleKey], extendableStyles[styleKey], theme, currentPath); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default function extendStyles( | ||
| baseStyleFn, | ||
| extendingStyleFn, | ||
| extendableStyles, | ||
| ) { | ||
| return (theme) => { | ||
| const baseStyle = baseStyleFn(theme); | ||
| const extendingStyle = extendingStyleFn(theme); | ||
|
|
||
| validateStyle(extendingStyle, extendableStyles, theme); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you think of a good way to surface this validation before runtime (e.g. in the editor and at build time)--maybe via TypeScript or ESLint? Most linter rules don't cross module boundaries, but there are some in eslint-plugin-import that do. |
||
|
|
||
| const styles = deepmerge(baseStyle, extendingStyle); | ||
|
|
||
| return styles; | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good if there was a babel plugin to remove these validator functions in production builds.