Skip to content

Commit

Permalink
Merge pull request #60 from 92green/feature/plugin-submit-errors
Browse files Browse the repository at this point in the history
break: reshape pluginsubmit api, store error state in plugin
  • Loading branch information
dxinteractive authored Oct 6, 2021
2 parents 10438f4 + 88ce0f4 commit ab7df43
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 104 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,15 +1175,16 @@ The `onSubmit` function passes the same `details` object as `onChange` does, so
import {PluginSubmit} from 'dendriform';

const plugins = {
submit: new PluginSubmit({
onSubmit: async (newValue, details) => {
submit: new PluginSubmit<V,E>({
onSubmit: async (newValue, details): void => {
// trigger save action here
// any errors will be eligible for resubmission
// diff(details) can be used in here to diff changes since last submit
},
onError: (error) => {
// optional function, will be called
// if an error occurs in onSubmit
onError: (error: any): E|undefined => {
// optional function, will be called if an error occurs in onSubmit
// anything returned will be stored in state in form.plugins.submit.error
// the error state is cleared on next submit
}
})
};
Expand All @@ -1204,12 +1205,12 @@ form.branch('foo').plugins.submit.changed;
PluginSubmit has the following properties and methods.
- `submit(): void` - submits the form if there are changes, calling `onSubmit`. If the value of the form has not changed then this has no effect.
- `previous: V` - the inital value / the value of the previous submit at the current branch.
- `usePrevous(): V` - a React hook returning the inital value / the value of the previous submit at the current branch.
- `dirty: boolean` - a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `useDirty(): boolean` - a React hook returning a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `submitting: boolean` - a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `useSubmitting(): boolean` - a React hook returning a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `previous: Dendriform<V>` - a dendriform containing the inital value / the value of the previous submit at the current branch.
- `submitting: Dendriform<boolean>` - a dendriform containing a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `submitting: Dendriform<E|undefined>` - a dendriform containing the most recent result of `onError`. This is changed to `undefined` on submit.
- `dirty.value: boolean` - a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `dirty.useValue(): boolean` - a React hook returning a boolean indicating if the value at the current branch is dirty i.e. has changed.
## Advanced usage
Expand Down
96 changes: 72 additions & 24 deletions packages/dendriform-demo/components/Demos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1489,13 +1489,18 @@ type SubmitValue = {
};

type SubmitPlugins = {
submit: PluginSubmit<SubmitValue>;
submit: PluginSubmit<SubmitValue,string>;
};

const causeAnErrorForm = new Dendriform(false);

async function fakeSave(value: SubmitValue): Promise<void> {
// eslint-disable-next-line no-console
console.log('saving', value);
await new Promise(r => setTimeout(r, 1000));
if(causeAnErrorForm.value) {
throw new Error('Error!');
}
// eslint-disable-next-line no-console
console.log('saved');
}
Expand All @@ -1511,7 +1516,8 @@ function PluginSubmitExample(): React.ReactElement {
submit: new PluginSubmit({
onSubmit: async (newValue: SubmitValue) => {
await fakeSave(newValue);
}
},
onError: e => e.message
})
});

Expand All @@ -1525,30 +1531,49 @@ function PluginSubmitExample(): React.ReactElement {
return <Region>
<form onSubmit={onSubmit}>
{form.render('firstName', form => {
const hasChanged = form.plugins.submit.useDirty();
const hasChanged = form.plugins.submit.dirty.useValue();
return <Region of="label">first name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</Region>;
})}

{form.render('lastName', form => {
const hasChanged = form.plugins.submit.useDirty();
const hasChanged = form.plugins.submit.dirty.useValue();
return <Region of="label">last name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</Region>;
})}

{form.render(form => {
const submitting = form.plugins.submit.useSubmitting();
const submitting = form.plugins.submit.submitting.useValue();
return <Region>
<button type="submit" disabled={!form.plugins.submit.useDirty()}>Submit</button>
<button type="submit" disabled={!form.plugins.submit.dirty.useValue()}>Submit</button>
{submitting && <span>Saving...</span>}
</Region>;
})}

{form.plugins.submit.error.render(form => {
const error = form.useValue();
return <Region>
{error && <code>{error}</code>}
</Region>;
})}
</form>

{causeAnErrorForm.render(form => (
<Region of="label">
cause an error on submit
<input type="checkbox" {...useCheckbox(form)} />
</Region>
))}
</Region>;
}

const PluginSubmitExampleCode = `
const causeAnErrorForm = new Dendriform(false);
async function fakeSave(value) {
console.log('saving', value);
await new Promise(r => setTimeout(r, 1000));
if(causeAnErrorForm.value) {
throw new Error('Error!');
}
console.log('saved');
}
Expand All @@ -1574,25 +1599,39 @@ function PluginSubmitExample() {
form.plugins.submit.submit();
}, []);
return <form onSubmit={onSubmit}>
{form.render('firstName', form => {
const hasChanged = form.plugins.submit.useDirty();
return <label>first name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</label>;
})}
return <>
<form onSubmit={onSubmit}>
{form.render('firstName', form => {
const hasChanged = form.plugins.submit.dirty.useValue();
return <label>first name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</label>;
})}
{form.render('lastName', form => {
const hasChanged = form.plugins.submit.useDirty();
return <label>last name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</label>;
})}
{form.render('lastName', form => {
const hasChanged = form.plugins.submit.dirty.useValue();
return <label>last name: <input {...useInput(form, 150)} /> {hasChanged ? '*' : ''}</label>;
})}
{form.render(form => {
const submitting = form.plugins.submit.useSubmitting();
return <>
<button type="submit" disabled={!form.plugins.submit.useDirty()}>Submit</button>
{submitting && <span>Saving...</span>}
</>;
})}
</form>;
{form.render(form => {
const submitting = form.plugins.submit.useSubmitting();
return <>
<button type="submit" disabled={!form.plugins.submit.dirty.useValue()}>Submit</button>
{submitting && <span>Saving...</span>}
</>;
})}
{form.plugins.submit.error.render(form => {
const error = form.useValue();
return <>{error && <code>{error}</code>}</>;
})}
</form>
{causeAnErrorForm.render(form => (
<label>
cause an error on submit
<input type="checkbox" {...useCheckbox(form)} />
</label>
))}
</>;
}
`;

Expand Down Expand Up @@ -2585,7 +2624,10 @@ const DEMOS: DemoObject[] = [
description: `An example of how one might implement drag and drop with react-beautiful-dnd. Dendriform's .renderAll() function, and its automatic id management on array elements simplifies this greatly.`,
anchor: 'draganddrop',
more: 'drag-and-drop'
},
}
];

const PLUGIN_DEMOS: DemoObject[] = [
{
title: 'Submit Plugin (PluginSubmit)',
Demo: PluginSubmitExample,
Expand Down Expand Up @@ -2667,6 +2709,12 @@ export function Demos(): React.ReactElement {
</Flex>;
}

export function PluginDemos(): React.ReactElement {
return <Flex flexWrap="wrap">
{PLUGIN_DEMOS.map(demo => <Demo demo={demo} key={demo.anchor} />)}
</Flex>;
}

export function AdvancedDemos(): React.ReactElement {
return <Flex flexWrap="wrap">
{ADVANCED_DEMOS.map(demo => <Demo demo={demo} key={demo.anchor} />)}
Expand Down
9 changes: 8 additions & 1 deletion packages/dendriform-demo/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import styled from 'styled-components';
import {Box, Flex, Wrapper, FloatZone} from '../components/Layout';
import {Text, H1, Link} from '../components/Text';
import {Demos, AdvancedDemos} from '../components/Demos';
import {Demos, PluginDemos, AdvancedDemos} from '../components/Demos';
import type {ThemeProps} from '../pages/_app';

export default function Main(): React.ReactElement {
Expand Down Expand Up @@ -36,6 +36,13 @@ export default function Main(): React.ReactElement {
<Demos />
</Box>
<Hr />
<Box mb={3}>
<H1>Plugin Demos</H1>
</Box>
<Box mb={3}>
<PluginDemos />
</Box>
<Hr />
<Box mb={3}>
<H1>Advanced Demos</H1>
</Box>
Expand Down
23 changes: 12 additions & 11 deletions packages/dendriform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,15 +1175,16 @@ The `onSubmit` function passes the same `details` object as `onChange` does, so
import {PluginSubmit} from 'dendriform';

const plugins = {
submit: new PluginSubmit({
onSubmit: async (newValue, details) => {
submit: new PluginSubmit<V,E>({
onSubmit: async (newValue, details): void => {
// trigger save action here
// any errors will be eligible for resubmission
// diff(details) can be used in here to diff changes since last submit
},
onError: (error) => {
// optional function, will be called
// if an error occurs in onSubmit
onError: (error: any): E|undefined => {
// optional function, will be called if an error occurs in onSubmit
// anything returned will be stored in state in form.plugins.submit.error
// the error state is cleared on next submit
}
})
};
Expand All @@ -1204,12 +1205,12 @@ form.branch('foo').plugins.submit.changed;
PluginSubmit has the following properties and methods.
- `submit(): void` - submits the form if there are changes, calling `onSubmit`. If the value of the form has not changed then this has no effect.
- `previous: V` - the inital value / the value of the previous submit at the current branch.
- `usePrevous(): V` - a React hook returning the inital value / the value of the previous submit at the current branch.
- `dirty: boolean` - a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `useDirty(): boolean` - a React hook returning a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `submitting: boolean` - a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `useSubmitting(): boolean` - a React hook returning a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `previous: Dendriform<V>` - a dendriform containing the inital value / the value of the previous submit at the current branch.
- `submitting: Dendriform<boolean>` - a dendriform containing a boolean stating if the plugin is currently waiting for an async `onSubmit` call to complete.
- `submitting: Dendriform<E|undefined>` - a dendriform containing the most recent result of `onError`. This is changed to `undefined` on submit.
- `dirty.value: boolean` - a boolean indicating if the value at the current branch is dirty i.e. has changed.
- `dirty.useValue(): boolean` - a React hook returning a boolean indicating if the value at the current branch is dirty i.e. has changed.
## Advanced usage
Expand Down
67 changes: 29 additions & 38 deletions packages/dendriform/src/plugins/PluginSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,44 @@ const isPromise = (thing: any): thing is Promise<unknown> => {
};

export type PluginSubmitOnSubmit<V> = (newValue: V, details: ChangeCallbackDetails<V>) => void|Promise<void>;
export type PluginSubmitOnError = (error: unknown) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PluginSubmitOnError<E> = (error: any) => E|undefined;

export type PluginSubmitConfig<V> = {
export type PluginSubmitConfig<V,E> = {
onSubmit: PluginSubmitOnSubmit<V>;
onError?: PluginSubmitOnError;
onError?: PluginSubmitOnError<E>;
};

type State<V> = {
type State<V,E> = {
form: Dendriform<V>;
previous: Dendriform<V>;
submitting: Dendriform<boolean>;
error: Dendriform<E|undefined>;
};

export class PluginSubmit<V> extends Plugin {
export class PluginSubmit<V,E=undefined> extends Plugin {

protected config: PluginSubmitConfig<V>;
state: State<V>|undefined;
protected config: PluginSubmitConfig<V,E>;
state: State<V,E>|undefined;

constructor(config: PluginSubmitConfig<V>) {
constructor(config: PluginSubmitConfig<V,E>) {
super();
this.config = config;
}

protected clone(): PluginSubmit<V> {
return new PluginSubmit<V>(this.config);
protected clone(): PluginSubmit<V,E> {
return new PluginSubmit<V,E>(this.config);
}

private getState(): State<V> {
private getState(): State<V,E> {
const {state} = this;
if(!state) die(8);
return state;
}

init(form: Dendriform<V>): void {
const submitting = new Dendriform(false);
const error = new Dendriform<E|undefined>(undefined);

const previous = new Dendriform(form.value, {history: 2});
previous.onChange((newValue, details) => {
Expand All @@ -56,10 +59,12 @@ export class PluginSubmit<V> extends Plugin {
const error = (e: unknown) => {
submitting.set(false);
previous.undo();
this.config.onError?.(e);
const errorResult = this.config.onError?.(e);
this.getState().error.set(errorResult);
};

try {
this.getState().error.set(undefined);
const result = this.config.onSubmit(newValue, details);
if(!isPromise(result)) {
return done();
Expand All @@ -75,7 +80,8 @@ export class PluginSubmit<V> extends Plugin {
this.state = {
form,
previous,
submitting
submitting,
error
};
}

Expand All @@ -92,37 +98,22 @@ export class PluginSubmit<V> extends Plugin {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
get previousForm(): Dendriform<any> {
get previous(): Dendriform<any> {
return this.getState().previous.core.getFormAt(this.path);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
get previous(): any {
return this.previousForm.value;
}

/* istanbul ignore next */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
usePrevious(): any {
return this.previousForm.useValue();
}

get dirty(): boolean {
return this.getForm().value !== this.previousForm.value;
}

/* istanbul ignore next */
useDirty(): boolean {
return this.getForm().useValue() !== this.previousForm.useValue();
get submitting(): Dendriform<boolean> {
return this.getState().submitting;
}

get submitting(): boolean {
return this.getState().submitting.value;
get error(): Dendriform<E|undefined> {
return this.getState().error;
}

/* istanbul ignore next */
useSubmitting(): boolean {
return this.getState().submitting.useValue();
get dirty(): {value: boolean, useValue: () => boolean} {
const value = !Object.is(this.getForm().value, this.previous.value);
/* istanbul ignore next */
const useValue = () => !Object.is(this.getForm().useValue(), this.previous.useValue());
return {value, useValue};
}

}
Loading

0 comments on commit ab7df43

Please sign in to comment.