@tejesh95
Tejesh P
Interaction Requirement
<div id ="filters">
<button type="button" class="active mb-4">China</button>
<button type="button" class="active mb-4">Nigeria</button>
<button type="button" class="active mb-4">India</button>
...
...
...
</div>
HTML
var all_regions_selected = true
Started Simple
var all_regions_selected = true
document.querySelector('#filters')
.addEventListener('click', function(event) {
if (event.target.tagName == 'BUTTON') {
}
})
Listen to button clicks on entire filters component
var all_regions_selected = true
document.querySelector('#filters')
.addEventListener('click', function(event) {
if (event.target.tagName == 'BUTTON') {
document.querySelectorAll('#filters button').forEach(function(node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
}
})
Listen to click events on entire filters component
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
if (document.querySelectorAll('#filters button.active').length === 6) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
}
else if (document.querySelectorAll('#filters button.active').length === 1 &&
document.querySelector('#filters button.active') == event.target) {
document.querySelectorAll('#filters button')
.forEach(function (node) {
node.classList.add('active')
})
}
}
})
One Button selected to All buttons selected
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
if (document.querySelectorAll('#filters button.active').length === 6) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
}
else if (document.querySelectorAll('#filters button.active').length === 1 &&
document.querySelector('#filters button.active') == event.target) {
document.querySelectorAll('#filters button')
.forEach(function (node) {
node.classList.add('active')
})
} else {
event.target.classList.toggle('active')
}
}
})
what about multi-selection?
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
if (document.querySelectorAll('#filters button.active').length === 6) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
}
else if (document.querySelectorAll('#filters button.active').length === 1 &&
document.querySelector('#filters button.active') == event.target) {
document.querySelectorAll('#filters button')
.forEach(function (node) {
node.classList.add('active')
})
} else {
event.target.classList.toggle('active')
}
}
})
SPOT THE BUG?
Almost Correct!
var all_regions_selected = true
document.querySelector('#filters')
.addEventListener('click', function (event) {
console.log(event.target, event.target.tagName)
if (event.target.tagName == 'BUTTON') {
if (all_regions_selected === true) {
document.querySelectorAll('#filters button').forEach(function(node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
all_regions_selected = false
}
else if (all_regions_selected === false) {
event.target.classList.toggle('active')
if (document.querySelectorAll('#filters button').length ===
document.querySelectorAll('#filters button.active').length) {
all_regions_selected = true
} else if (document.querySelectorAll('#filters button.active').length == 0) {
document.querySelectorAll('#filters button').forEach(function (node) {
node.classList.add('active')
})
}
}
}
})
Final Code
Data structures are far more robust than code
{
state: {
action: function() {}
},
state: {
action: function() {}
},
state: {
action: function() {}
},
...
...
}
What if we had a better Data Structure?
{
'All Regions Selected': {
click: function (event) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.target) {
node.classList.remove('active')
}
})
}
},
'More than One Region Selected': {
click: function (event) {
event.target.classList.toggle('active')
}
},
}
This Mapping is actually a State Machine
What is a State Machine?
Current State | Event | Next State |
---|---|---|
One Region Selected | click | More than One Region Selected |
All Regions Selected | click | One Region Selected |
More than One Region Selected | click | More than One Region Selected |
More than One Region Selected | click | All Regions Selected |
One Region Selected | click | All Regions Selected |
Current State | Event | Next State |
---|---|---|
One Region Selected | click | More than One Region Selected |
All Regions Selected | click | One Region Selected |
More than One Region Selected | click | More than One Region Selected |
More than One Region Selected | click | All Regions Selected |
One Region Selected | click | All Regions Selected |
State Chart
State Chart
- Richard Feynman
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
input('click', event)
}
})
const input = function (event_name, args) {
const state = machine.currentState
// Action dispatcher
actions[machine.currentState][event_name](args)
// State Transition(er)
machine.currentState = machine.states[state][event_name](args)
// On State Event Triggers
stateEvents[machine.currentState]()
// Time Travel Logger
console.log(`${state} + ${event_name} --> ${machine.currentState}`)
}
const machine = {
currentState: 'All Regions Selected',
states: {
'All Regions Selected': {
CLICK: function () {
return 'One Region Selected'
}
},
'One Region Selected': {
CLICK: function () {
if (....) {
return 'All Regions Selected'
}
return 'More than One Region Selected'
}
},
....
....
const machine = {
currentState: 'All Regions Selected',
states: {
'All Regions Selected': {
CLICK: function () {
return 'One Region Selected'
}
},
'One Region Selected': {
CLICK: function () {
if (document.querySelectorAll('#filters button').length ===
document.querySelectorAll('#filters button.active').length) {
return 'All Regions Selected'
}
return 'More than One Region Selected'
}
},
....
....
const stateEvents = {
'All Regions Selected': function() {
$('#conditional button').prop("disabled", true)
},
'One Region Selected': function() {
$('#conditional button').prop("disabled", false)
},
'More than One Region Selected': function() {
$('#conditional button').prop("disabled", true)
}
}
const filterMachine = XState.Machine(
{
id: 'click',
initial: 'All Regions Selected',
states: {
'All Regions Selected': {
on: {
CLICK: [
{
target: 'One Region Selected',
actions: ['deselect_other_than_clicked_button']
}
]
}
},
'More than One Region Selected': {
entry: ['toggle_button_state'],
on: {
CLICK: [
{
target: 'All Regions Selected',
cond: 'is_all_buttons_selected',
actions: ['toggle_button_state']
},
{
target: 'One Region Selected',
// clicked the second active button
cond: 'is_one_button_selected',
actions: ['toggle_button_state']
},
{
target: 'More than One Region Selected'
}
]
}
},
'One Region Selected': {
on: {
CLICK: [
{
target: 'All Regions Selected',
cond: 'is_theonlyactive_button_clicked',
actions: ['select_all']
}
]
}
}
}
},
{
actions: {
deselect_other_than_clicked_button: function (context, event, actionMeta) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.value.target) {
node.classList.remove('active')
}
})
},
select_all: function () {
document.querySelectorAll('#filters button').forEach(function (node) {
node.classList.add('active')
})
},
toggle_button_state: function (context, event, actionMeta) {
event.value.target.classList.toggle('active')
}
},
guards: {
is_all_buttons_selected: function (context, event) {
return (document.querySelectorAll('#filters button').length - 1===
document.querySelectorAll('#filters button.active').length) &&
event.value.target == document.querySelector('#filters button:not(.active)')
},
is_one_button_selected: function (ctx, event) {
return document.querySelectorAll('#filters button.active').length === 2 && event.value.target.classList.contains('active')
},
is_theonlyactive_button_clicked: function (context, event) {
return document.querySelector('#filters button.active') == event.value.target
}
}
}
)
var filterService = XState.interpret(filterMachine)
.onTransition((state) => console.log(state))
.start()
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
filterService.send({ type: 'CLICK', value: event })
}
})
const filterMachine = XState.Machine(
{
id: 'click',
initial: 'All Regions Selected',
states: {
'All Regions Selected': {
on: {
CLICK: [
{
target: 'One Region Selected',
actions: ['deselect_other_than_clicked_button']
}
]
}
},
'More than One Region Selected': {
entry: ['toggle_button_state'],
on: {
CLICK: [
{
target: 'All Regions Selected',
cond: 'is_all_buttons_selected',
actions: ['toggle_button_state']
},
{
target: 'One Region Selected',
// clicked the second active button
cond: 'is_one_button_selected',
actions: ['toggle_button_state']
},
{
target: 'More than One Region Selected'
}
]
}
},
'One Region Selected': {
on: {
CLICK: [
{
target: 'All Regions Selected',
cond: 'is_theonlyactive_button_clicked',
actions: ['select_all']
}
]
}
}
}
},
{
actions: {
deselect_other_than_clicked_button: function (context, event, actionMeta) {
document.querySelectorAll('#filters button').forEach(function (node) {
if (node !== event.value.target) {
node.classList.remove('active')
}
})
},
select_all: function () {
document.querySelectorAll('#filters button').forEach(function (node) {
node.classList.add('active')
})
},
toggle_button_state: function (context, event, actionMeta) {
event.value.target.classList.toggle('active')
}
},
guards: {
is_all_buttons_selected: function (context, event) {
return (document.querySelectorAll('#filters button').length - 1===
document.querySelectorAll('#filters button.active').length) &&
event.value.target == document.querySelector('#filters button:not(.active)')
},
is_one_button_selected: function (ctx, event) {
return document.querySelectorAll('#filters button.active').length === 2 && event.value.target.classList.contains('active')
},
is_theonlyactive_button_clicked: function (context, event) {
return document.querySelector('#filters button.active') == event.value.target
}
}
}
)
var filterService = XState.interpret(filterMachine)
.onTransition((state) => console.log(state))
.start()
document.querySelector('#filters')
.addEventListener('click', function (event) {
if (event.target.tagName == 'BUTTON') {
filterService.send({ type: 'CLICK', value: event })
}
})
simplifies the logic and makes it more predictable
debugging code is easy
when code goes to unpredicted state, we can trace the reason to previous states
less code to cover while testing
Needs upfront thinking - not needed for simple scenarios
Using a statechart increases the number of lines of code