@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 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...?
User flows are transitions
between UI states
due to events
A state is a dynamic property expressing characteristics of an object that may change in response to user action or automated processes.Β
States do not affect the essential nature of the object, but represent data associated with the object or user interaction possibilities.
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?
<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>
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;
}
}
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
}
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
Idle
Searching...
entry / prefetchResources
entry / fetchResults
Search
[query.length > 0]
H
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
SEARCH
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
Searched
Idle
Searching...
SEARCH
Success
Failure
RESOLVE
REJECT
SEARCH
Searched
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 π
Statecharts
FSMs
Bottom-up
States & logic
Code Complexity
@davidkpiano Β· CSSConf Budapest 2019