Last active
March 4, 2020 15:58
-
-
Save the-main-thing/0e9dba5cbef93a36114024f4282e3ecc to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const id = 'databoard-inline-realtime-update' | |
const initialContext = { | |
deletingKey: null, | |
data: [], | |
currentItem: { | |
key: null | |
}, | |
invalidInputs: {}, | |
errorMessage: null | |
}; | |
const initialEvents = { | |
ADD: { | |
target: `#${id}.edit.new`, | |
actions: ['onAdd'] | |
}, | |
EDIT: { | |
target: `#${id}.edit.change`, | |
actions: ['onEdit'] | |
}, | |
DELETE: { | |
target: 'deleting', | |
actions: ['onDelete'] | |
}, | |
DATA_UPDATE: { | |
target: 'idle', | |
actions: ['updateData'] | |
} | |
}; | |
const deletingState = (errorStateName, initialStateName) => { | |
return { | |
initial: 'confirm', | |
states: { | |
confirm: { | |
on: { | |
CONFIRM: 'deleting', | |
CANCEL: initialStateName, | |
DATA_UPDATE: [ | |
{ | |
// Docs that we working with is not affected by update, so keep state unchanged. | |
in: `#${id}.edit.change`, | |
cond: 'editingEntryAndDeletingEntryStillExists', | |
actions: ['updateData', 'updateEditingEntry'] | |
}, | |
{ | |
// Entry we trying delete has been deleted | |
in: `#${id}.edit.change`, | |
cond: 'editingEntryStillExists', | |
target: 'outdated', | |
actions: ['updateData', 'updateEditingEntry'] | |
}, | |
{ | |
// Entry we trying to delete and entry we trying to change is not existing anymore. | |
in: `#${id}.edit.change`, | |
target: `#${id}.edit.change.outdated`, | |
actions: ['updateData'] | |
}, | |
{ | |
// For non edit.change cases if deleting entry is not deleted yet, do not transition. | |
cond: 'deletingEntryStillExists', | |
actions: ['updateData'] | |
}, | |
{ | |
// Non edit.change case and deleting entry does not exists no more. | |
target: 'outdated', | |
actions: ['updateData'] | |
} | |
] | |
} | |
}, | |
deleting: { | |
invoke: { | |
src: 'remove', | |
onDone: [ | |
{ | |
in: `#${id}.edit.change`, | |
cond: 'deletedCurrentlyEditingItem', | |
target: `#${id}.idle`, | |
actions: ['resetDeleteKey'] | |
}, | |
{ | |
// just deleted item that we was editing | |
in: `#${id}.edit.change`, | |
target: initialStateName, | |
actions: ['resetDeleteKey'] | |
}, | |
{ | |
target: initialStateName, | |
actions: ['resetDeleteKey'] | |
} | |
], | |
onError: { | |
target: errorStateName | |
} | |
}, | |
on: { | |
DATA_UPDATE: { | |
actions: ['updateData'] | |
} | |
} | |
}, | |
outdated: { | |
on: { | |
CANCEL: initialStateName | |
} | |
} | |
} | |
}; | |
}; | |
const editingEvents = { | |
SAVE: { | |
target: 'checkingInputs' | |
}, | |
CHANGE: { | |
target: 'idle', | |
actions: ['onChange'] | |
}, | |
DATA_UPDATE: [ | |
{ | |
in: `#${id}.edit.new`, | |
target: 'idle', | |
actions: ['updateData'] | |
}, | |
{ | |
in: `#${id}.edit.change`, | |
cond: 'editingEntryStillExists', | |
target: 'idle', | |
actions: ['updateData', 'updateEditingEntry'] | |
}, | |
{ | |
in: `#${id}.edit.change`, | |
target: 'outdated', | |
actions: ['updateData'] | |
} | |
], | |
DELETE: { | |
target: 'deleting', | |
actions: ['onDelete'] | |
}, | |
CANCEL: { | |
target: `#${id}.idle`, | |
actions: ['clearContext'] | |
} | |
}; | |
const getEditingState = ({ saveFnName, checkFnName, rootStateName }) => { | |
return { | |
initial: 'idle', | |
states: { | |
idle: { | |
on: { | |
...editingEvents | |
} | |
}, | |
checkingInputs: { | |
invoke: { | |
src: checkFnName, | |
onDone: { | |
target: 'saving' | |
}, | |
onError: { | |
target: 'savingError', | |
actions: ['onCheckingInputsError'] | |
} | |
}, | |
on: { | |
DATA_UPDATE: [ | |
{ | |
in: `#${id}.edit.change`, | |
cond: 'editingEntryStillExists', | |
actions: ['updateData'] | |
}, | |
{ | |
in: `#${id}.edit.change`, | |
target: `#${id}.edit.new.savingError`, | |
actions: ['onDataRemovedWhileSaving', 'updateData'] | |
}, | |
{ | |
actions: ['updateData'] | |
} | |
] | |
} | |
}, | |
saving: { | |
invoke: { | |
src: saveFnName, | |
onDone: { | |
target: `#${id}.idle`, | |
actions: ['clearContext'] | |
}, | |
onError: { | |
target: 'savingError', | |
actions: ['onSavingError'] | |
} | |
}, | |
on: { | |
DATA_UPDATE: [ | |
{ | |
in: `#${id}.edit.change`, | |
cond: 'editingEntryStillExists', | |
actions: ['updateData'] | |
}, | |
{ | |
in: `#${id}.edit.change`, | |
target: `#${id}.edit.new.savingError`, | |
actions: ['onDataRemovedWhileSaving', 'updateData'] | |
}, | |
{ | |
actions: ['updateData'] | |
} | |
] | |
} | |
}, | |
savingError: { | |
on: { | |
...editingEvents | |
} | |
}, | |
deleting: deletingState( | |
`${rootStateName}.deletingError`, | |
`#${rootStateName}.idle` | |
), | |
deletingError: { | |
on: { | |
...editingEvents | |
} | |
}, | |
outdated: { | |
on: { | |
CANCEL: [ | |
{ | |
in: `#${id}.edit.change`, | |
target: `#${id}.idle` | |
} | |
] | |
} | |
} | |
} | |
}; | |
}; | |
const stateMachine = Machine( | |
{ | |
id, | |
initial: 'idle', | |
context: initialContext, | |
states: { | |
idle: { | |
initial: 'idle', | |
states: { | |
idle: { | |
on: { | |
...initialEvents | |
} | |
}, | |
deletingError: { | |
on: { | |
...initialEvents | |
} | |
}, | |
deleting: deletingState(`#${id}.idle.deletingError`, `#${id}.idle.idle`) | |
} | |
}, | |
edit: { | |
states: { | |
new: getEditingState({ | |
saveFnName: 'saveNew', | |
checkFnName: 'checkNew', | |
rootStateName: `#${id}.edit.new` | |
}), | |
change: getEditingState({ | |
saveFnName: 'saveChange', | |
checkFnName: 'checkChange', | |
rootStateName: `#${id}.edit.change` | |
}) | |
} | |
} | |
} | |
}, | |
{ | |
guards: { | |
deletingEntryStillExists: (context, event) => { | |
const { deletingKey } = context; | |
const { data = [] } = event; | |
return data.some((item) => item.key === deletingKey); | |
}, | |
editingEntryStillExists: (context, event) => { | |
const { key } = context.currentItem; | |
const { data = [] } = event; | |
return data.some((item) => item.key === key); | |
}, | |
deletedCurrentlyEditingItem: (context) => { | |
const { deletingKey } = context; | |
const { key } = context.currentItem; | |
return deletingKey === key; | |
}, | |
editingEntryAndDeletingEntryStillExists: (context, event) => { | |
const { deletingKey } = context; | |
const { key } = context.currentItem; | |
const { data = [] } = event; | |
const deletingEntryExitst = data.some((item) => item.key === deletingKey); | |
const editingEntryStillExists = data.some((item) => item.key === key); | |
return deletingEntryExitst && editingEntryStillExists; | |
} | |
}, | |
actions: { | |
updateData: assign({ | |
data: (_, event) => { | |
return event.data || []; | |
} | |
}), | |
// When data is updated get new values for editing entry | |
updateEditingEntry: assign((context, event) => { | |
const { currentItem } = context; | |
const { key } = currentItem; | |
const { data = [] } = event; | |
const updatedEntry = data.find((item) => item.key === key); | |
if (updatedEntry === undefined) { | |
// We guard transitions to this code must not be executed ever. | |
throw new Error('Cant find currently editing entry in updated data array'); | |
} | |
return { | |
...context, | |
currentItem: { | |
key, | |
...currentItem, | |
...updatedEntry | |
} | |
}; | |
}), | |
clearContext: assign((context) => { | |
const { data } = context; | |
return { | |
...initialContext, | |
data | |
}; | |
}), | |
onAdd: assign((context, event) => { | |
const { currentItem } = event; | |
return { | |
...context, | |
currentItem: { | |
...context.currentItem, | |
...currentItem | |
} | |
}; | |
}), | |
onEdit: assign((context, event) => { | |
const { currentItem } = event; | |
return { | |
...context, | |
currentItem: { | |
...context.currentItem, | |
...currentItem | |
} | |
}; | |
}), | |
onDelete: assign({ | |
deletingKey: (_, event) => event.deletingKey | |
}), | |
onChange: assign((context, event) => { | |
const { currentItem } = event; | |
return { | |
...context, | |
errorMessage: null, | |
invalidInputs: {}, | |
currentItem: { ...context.currentItem, ...currentItem } | |
}; | |
}), | |
resetDeleteKey: assign({ | |
deletingKey: () => null | |
}), | |
onCheckingInputsError: assign({ | |
invalidInputs: (_, event) => { | |
return event.data.invalidInputs || {}; | |
}, | |
errorMessage: (_, event) => { | |
return event.data.errorMessage || ''; | |
} | |
}), | |
onSavingError: assign({ | |
errorMessage: (_, event) => | |
event.data.errorMessage || | |
event.data.message || | |
'Неизвестная ошибка при сохранении', | |
invalidInputs: (_, event) => event.data.invalidInputs || {} | |
}), | |
onDataRemovedWhileSaving: assign({ | |
errorMessage: () => | |
'Не успели сохранить запись, кто-то (вы?) только что удалил её' | |
}) | |
} | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment