I've got a question about composition I'm hoping someone can help me with.
I'm using react-css-modules
with Sass, and I'd like to know the best way to compose things for one of our basic bottom-level components.
Here's our component:
import React, {PropTypes} from 'react'
import cssModules from 'react-css-modules'
import styles from './style.sass'
const Button = ({children = 'Submit', ...props}) => {
const align = props.align ? `-${props.align}` : ''
const type = props.type ? `-${props.type}` : ''
const styleName = `button${type}${align}`
return (
<button onClick={props.onClick} {...props} styleName={styleName}>
{children}
</button>
)
}
Button.propTypes = {
align: PropTypes.string,
onClick: PropTypes.func.isRequired,
type: PropTypes.string,
}
export default cssModules(Button, styles)
And here's the stylesheet so far:
@import "~components/styles/variables"
.button
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
.left
float: left
.right
float: right
.primary
color: $background-interaction
background-color: $button-default
.button-left
composes: button, left
.button-right
composes: button, right
.button-primary
composes: button, primary
.button-primary-left
composes: button, primary, left
.button-primary-right
composes: button, primary, right
Right now, it's pretty painful. Every configurable prop we add exponentially increases the number of composed classes we have to provide. We can currently configure align
and type
, and since both can be null we have 6 possible combinations, so 5 composed classes to create in addition to the base .button
.
If we added just one more prop, say just a boolean bold
, we now have to add a whole bunch of new composed class names: .button-bold, .button-left-bold, .button-right-bold, .button-primary-bold, .button-primary-left-bold, .button-primary-right-bold
.
I know with react-css-modules
we can just enable the allowMultiple
setting to allow us to specify multiple modules to apply to an element, but my understanding is that is against best practices. I feel like we have to be missing something here. What are we doing wrong?
A CSS Module is a CSS file in which all class names and animation names are scoped locally by default. CSS Modules let you write styles in CSS files but consume them as JavaScript objects for additional processing and safety.
CSS Modules are "CSS files in which all class names and animation names are scoped locally by default". Instead of having CSS files and classes that are static, CSS Modules creates a dynamic implementation that is locally scoped to the HTML with the help of Webpack or Browserify.
To import a CSS Module into the corresponding component, import the css modules stylesheet as styles or [name]Styles : In JSX, use the defined CSS class as a className prop like so: From here, you can add as many other CSS modules you'd like, just remember to import each as a different name.
I think maybe there's a mix of concerns in the example, and that's why it doesn't fit the "one class per element" rule. One of the reasons to enforce the one class per element rule is so that we can easily change the appearance of an element's state without actually touching the element. (Basically the promise of CSS itself, finally realized.) If you have multiple classes on an element, it's difficult to control the appearance without changing the element. This is especially true if you use appearance (rather than semantic) classes.
A class like "button-primary-left-bold" has some semantic meaning ("button-primary"), but it also has some layout meaning ("left") and some text appearance meaning ("bold"). A Button component probably has no business controlling its own layout. Remember, you can compose React components too! So you can have something more like:
<Left><Button type="primary">Click Me!</Button></Left>
Now the CSS module for the Button component can worry just about the types of buttons. And the Button component can be used anywhere, with any layout, not just a float-based layout.
Even better, the float could be pushed into a more semantic component as well. Perhaps something like:
<ButtonBar>
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</ButtonBar>
The ButtonBar can have its own CSS module that does the layout. And later you can swap out that janky float layout for a swanky flexbox layout. :-)
Your "bold" modifier example certainly has no place inside the Button component. Better to think about why something is bold, and then make a component or semantic property for that. Otherwise, if the design calls for changing those "bold" buttons to "italic green" buttons, you have to go around changing a bunch of elements.
If you do this (exchange visual/layout classes for semantic components and break up components and CSS modules), you'll have less "exponential increases". If you do end up in a situation where you really do have a need for multiple semantic properties being combined, there is still some value in having those states be spelled out. A good example might be "primary-disabled". This is better than "primary disabled" for a couple of reasons. First, it's easy to look in the CSS module and see the explicit state listed. Second, there aren't ambiguous uses and relationships of those classes. Is "primary disabled" actually a valid use of those classes? That can only be known if somebody documents that use. The "disabled" class may override something in the "primary" class, meaning there are implicit ordering dependencies. It's easy for a well-meaning edit in the future to break things, because the relationship between those classes isn't obvious. As is often the case, choosing something implicit to save some keystrokes can lead to subtle errors. Paying that bit of keystroke tax locks things in and makes it obvious what will work and what won't.
Another small point that will save you a few keystrokes: there's really no reason for the "button-" prefix. That's exactly what CSS modules are for. Do this instead:
.normal
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
.primary
composes: normal
color: $background-interaction
background-color: $button-default
The filename itself essentially is the "button" prefix.
I think I'd move away from compose in this case and nest your classes. Here's my suggestion (pardon me if my jsx is a tad off):
import React, {PropTypes} from 'react'
import cssModules from 'react-css-modules'
import styles from './style.sass'
const Button = ({children = 'Submit', ...props}) => {
const align = props.align ? `${props.align}` : ''
const type = props.type ? `${props.type}` : ''
const styleName = `button ${type} ${align}`
return (
<button onClick={props.onClick} {...props} styleName={styleName}>
{children}
</button>
)
}
Button.propTypes = {
align: PropTypes.string,
onClick: PropTypes.func.isRequired,
type: PropTypes.string,
}
export default cssModules(Button, styles)
And the SASS:
@import "~components/styles/variables"
.button
color: $button-default
background-color: transparent
font-family: $font-family
font-size: $default-font-size
font-weight: $font-regular
line-height: $default-button-height
margin: 0 $pad 0 0
outline: none
padding: 0 $pad*2
&.left
float: left
&.right
float: right
&.primary
color: $background-interaction
background-color: $button-default
Fully acknowledging that "left" and "primary" could conflict with other class names in your app. So it might not be a bad idea to come up with some slightly better (more scoped) names.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With