Skip to content

Instantly share code, notes, and snippets.

@the-main-thing
Last active March 4, 2020 15:58
Show Gist options
  • Save the-main-thing/0e9dba5cbef93a36114024f4282e3ecc to your computer and use it in GitHub Desktop.
Save the-main-thing/0e9dba5cbef93a36114024f4282e3ecc to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
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