Crafting Stateful Styles
@davidkpiano · dotCSS 2019
Fetch data
Fetch data
Fetch data
Fetch data
Normal
Hover
Active
Disabled
Fetched data
Success
Failed to fetch
Error
Fetching data...
Loading
Fetching data...
Loading
<button class="button loading">
Fetching data...
</button>
I wasn't always good at JavaScript.
input[type="checkbox"] {
/* ... */
}
input[type="checkbox"] ~ #app {
/* ... */
}
input[type="checkbox"] ~ #app .box {
background-color: white;
}
input[type="checkbox"]:checked
~ #app .box {
background-color: blue;
}
~
checkbox
#app
.box
~
Adjacent sibling selector
label for="..."
label for="..."
checkboxes
radios
Zero or more
One at a time
Fetching data...
<button class="loading">
Fetching data...
</button>
<button class="success">
Fetched data!
</button>
<button class="loading success">
Fetched data...?
</button>
Fetched data!
Fetched data...?
Is there a better way to model
state for dynamic UIs?
User flows are transitions
between UI states
due to events
Finite state machines
and statecharts
A state is a dynamic property expressing characteristics of an object that may change in response to user action or automated processes.
- aria-busy
- aria-current
- aria-disabled
- aria-grabbed
- aria-hidden
- aria-invalid
States do not affect the essential nature of the object, but represent data associated with the object or user interaction possibilities.
:state(...) {
/* ... */
}
:hover
:invalid
:active
:focus
:empty
:checked
Data Attributes
<button data-state="loading">
Fetching data...
</button>
button[data-state="loading"] {
opacity: 0.5;
}
elButton.dataset.state; // 'loading'
elButton.dataset.state = 'success';
delete elButton.dataset.state;
HTML
CSS
JavaScript
<button data-state="success">
Fetched data!
</button>
<button>
Fetch data
</button>
Data Attributes
Fetching data...
<button data-state="idle">
Fetch data
</button>
<button data-state="loading">
Fetching data...
</button>
<button data-state="success">
Fetched data!
</button>
<button data-state="error">
Failed to fetch data
</button>
Fetched data!
Failed to fetch
Fetch data
Loading
Success
Resolve
Initial state
States
Events
Transitions
Final State
Failed
REJECT
RETRY
Finite state machine
Define transitions between
states & events
function searchReducer(state = 'idle', event) {
switch (state) {
case 'idle':
switch (event.type) {
case 'SEARCH':
return 'searching';
default:
return state;
}
case 'searching':
switch (event.type) {
case 'RESOLVE':
return 'success';
case 'REJECT':
return 'failure';
default:
return state;
}
case 'success':
switch (event.type) {
case 'SEARCH':
return 'searching';
default:
return state;
}
case 'failure':
switch (event.type) {
case 'SEARCH':
return 'searching';
default:
return state;
}
default:
return state;
}
}
FSMs with switch/case
const machine = {
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' }
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
};
Define transitions between
states & events
const transition = (state, event) => {
return machine
.states[state] // current state
.on[event] // next state
|| state; // or same state
}
FSMs with object mapping
npm install xstate
const machine = {
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' }
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
};
import { Machine } from 'xstate';
const machine = Machine({
initial: 'idle',
states: {
idle: {
on: { SEARCH: 'searching' }
},
searching: {
on: {
RESOLVE: 'success',
REJECT: 'failure',
SEARCH: 'searching'
}
},
success: {
on: { SEARCH: 'searching' }
},
failure: {
on: { SEARCH: 'searching' }
}
}
});
idle
dragging
pan
panstart
panend
(assign dx, dy)
(assign x, y)
<main data-state="loading" data-prev-state="idle">
idle
loading
error
<main data-state="loading" data-prev-state="error">
const elApp = document.querySelector('#app');
function setAppState(state) {
// change data-state attribute
elApp.dataset.prevState = elApp.dataset.state;
elApp.dataset.state = state;
// ...
}
✨✨✨
💥💥💥
[data-state="loading"][data-prev-state="idle"] {
/* loading initially */
}
[data-state="loading"][data-prev-state="error"] {
/* loading after error */
}
<main
data-state="success"
data-transition="loading success"
>
</main>
[data-transition^="loading"] {
/* from "loading" */
}
[data-transition="loading success"] {
/* from "loading" to "success" */
}
HTML
CSS
previous state
current state
start
selecting
selected
dragging
disposed
grabbing
disposed
data-state="idle"
data-show="loading"
data-hide="loading"
data-active
data-active
active when app is in "loading" state
inactive when app is in "loading" state
data-state="loading"
const elApp = document.querySelector('#app');
function setAppState(state) {
// change data-state attribute
elApp.dataset.state = state;
// remove any active data-attributes
document.querySelectorAll(`[data-active]`)
.forEach(el => {
delete el.dataset.active;
});
// add active data-attributes to proper elements
document
.querySelectorAll([
`[data-show~="${state}"]`,
`[data-hide]:not([data-hide~="${state}"])`
].join(','))
.forEach(el => {
el.dataset.active = true;
});
}
// set button state to 'loading'
setAppState('loading');
[data-show~="${state}"]
[data-hide]:not([data-hide~="${state}"])
Think in transitions
not just events,
not just state.
The world of statecharts
Spectrum community
XState Documentation
Make your code do more.
@davidkpiano · DotCSS 2019
Thank you dotCSS!
Crafting Stateful Styles
By David Khourshid
Crafting Stateful Styles
DotCSS
- 7,039