with STate Machines
Crafting Stateful Styles
@davidkpiano Β· CSSConf Budapest 2019
Fetch data
Fetch data
Fetch data
Fetch data
Normal
Hover
Active
Disabled
Fetched data
Success
Failed to fetch
Error
Fetching data...
Loading
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
Fetch data
π
GET api/users
Pending
200 OK
Fetch data
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
π
GET api/users
Pending
Fetch data
I wasn't always good at JavaScript.
I still sorta suck
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
Sorry, conference wifi
(pretend this is a loading spinner β³)
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.
Finite state machines
-
have one initial state
-
a finite number of states
-
a finite number of events
-
a mapping of state transitionsΒ
triggered by events -
a finite number of final states
Idle
Pending
Rejected
Fulfilled
Fetch
Resolve
reject
Fulfilled
Rejected
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
SEARCH
π«
SEARCH?
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
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' }
}
}
});
import { Machine } from 'xstate';
const machine = Machine({
// ...
});
const searchingState = machine
.transition('idle', 'searching');
searchingState.value;
// => 'searching'
idle
dragging
pan
panstart
panend
(assign dx, dy)
(assign x, y)
Don't get too excited it's literally just a square
<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 */
}
This one will be a cooler demo I hope
start
selecting
selected
dragging
disposed
grabbing
This will load by the weekend
Does this scale?
State explosion problem
Statecharts
(Hierarchical State Machines)
Statecharts
Idle
Searching...
entry / prefetchResources
entry / fetchResults
Search
- ActionsΒ - onEntry, onExit, transition
- GuardsΒ - conditional transitions
[query.length > 0]
Statecharts
- ActionsΒ - onEntry, onExit, transition
- GuardsΒ - conditional transitions
- HierarchyΒ - nested states
- OrthogonalityΒ - parallel states
- HistoryΒ - remembered states
H
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
Hierarchical states
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
Searched
Hierarchical states
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
Searched
Hierarchical states
const machine = Machine({
initial: 'idle',
states: {
// ...
searching: {
on: {
RESOLVE: 'searched',
REJECT: 'searched.failure'
}
},
searched: {
initial: 'success',
states: {
success: {},
failure: {}
},
on: { SEARCH: 'searching' }
}
}
});
#app[data-state] {
/* has attribute */
}
#app[data-state="idle"] {
/* attribute equals exactly */
}
#app[data-state*="err"] {
/* attribute contains */
}
#app[data-state^="idle"] {
/* attribute starts with */
}
#app[data-state$="error"] {
/* attribute ends with */
}
#app[data-state~="idle.error"] {
/* attribute contains (spaced) */
}
<main id="app" data-state="idle">
<!-- ... -->
</main>
<main id="app" data-state="idle idle.error">
<!-- ... -->
</main>
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}"])
Okay who is NPM installing π
Think in states + events,
not just events.
Advantages
of using statecharts
- Visualized modeling
- Precise diβagrams
- Automatic code generation
- Comprehensive test coverage
- Accommodation of late-breaking requirements changes
Disadvantages
of using statecharts
Learning curve
Modeling requires planning ahead
Not everything can be modeled (yet)
Statecharts
FSMs
Bottom-up
Complexity
trade-offs
States & logic
Code Complexity
The world of statecharts
Spectrum community
Make your code do more.
Thank you CSSConf Budapest!
@davidkpiano Β· CSSConf Budapest 2019
Crafting Stateful Styles
By David Khourshid
Crafting Stateful Styles
CSSConf Budapest 2019
- 4,240