Presentation is an important part of a UI component. Diffing makes it signifcantly easier to build well-encapsulated components, but styles still pose problems. CSS is global and hard to modularize. For simple cases, inline styles fix the problem easily:
var h = require('virtual-dom/h')
function render (state) {
var style = {
color: state.enabled ? 'red' : undefined
}
return h('p', {style: style}, 'Hello world!')
}
The only challenge around simple CSS is when using long lists with server-side rendering (solvable). Prefixing can be addressed via npm packages.
The more difficult cases are the features that can't be used in inline styles:
- @media queries
- @keyframe animations
- pseudo-selectors
matchMedia
to the rescue! Browser support is good and we get the benefit of using inline styles and JS instead of writing CSS.
We can integrate matchMedia
with a diffable UI as follows:
- Use an immutable data structure in your application like observ or Immutable.
- Use memoization to prevent unnecessary calls to your rendering functions (e.g. vdom-thunk).
- Create two immutatable states in your application: one for application state (e.g. user is logged in) and one for transient state (e.g. screen width < 500px).
- Use
matchMedia
with a set of predefined breakpoints (e.g. ergonomic-breakpoint) and update your transient state object in your handler (matchMedia(query).addListener(handler)
). - Pass your transient state into the render function of components, generally preceded by your application state.
- Render inline styles conditionally depending on the value of the matched media (transient state).
There are some solvable implementation challenges, including the need for a singleton delegator a la dom-delegator.
It's typically easier to get performant CSS animations versus pure JavaScript animation. If we can accomplish the following, we can stay modular while still using CSS:
- Build a CSS string in JavaScript, giving the keyframe a random name by appending a cuid
- Insert the CSS into a
<style>
tag - Export the keyframe name from the module
- require it
Example:
bounce.js
var bounce = createKeyframes({/* keyframes to bounce element */})
require('insert-css')(bounce.css)
module.exports = bounce.name
component.js
var bounce = require('./bounce')
function render () {
return h('button', {
style: {
animation: bounce + ' 2s infinite'
}
})
}
You should be able to easily avoid the use of virtually all pseudo-selectors with relatively simple JavaScript.
Take alternating row colors in a table:
tr {
background: white
}
tr:nth-child(even) {
background: gray
}
Instead:
function render (rows) {
return rows.map(function (row, index) {
return h('tr', {
style: {
background: index % 2 ? 'gray' : 'white'
}
}, row.map(renderColumn))
})
}
function renderColumn (data) {
// ...
}
Thoughts
The pseudo selector bit is very clever!
Maybe list the ones that you have in mind?
Why not just use one state object? Seems very reasonable for screen dimensions to be a part of your application's state.
When would you need a singleton delegator?
I don't (yet) think that writing non-modular CSS is anywhere near as much of a you'll-pay-for-this-when-you-want-to-add-more-features-later as, say, non-modular JS or markup/hyperscript.
So while this looks like a cleaner approach, I'd love for this draft to give me a better sense of what my gains are here. I'd love for this draft to really convince me of the benefits of keeping as much styling as I can in JavaScript as opposed to trying to tackle modularity with a pre-processor such as Sass.
ex:
Why exactly do I care?
Awesome read!