Skip to content

Commit

Permalink
Allow Asynchronous Models (#30)
Browse files Browse the repository at this point in the history
* initial commit for Allow Asynchronous Models

* allow async models

* allow more modern versions of react

* rework dependencies

* add strict equality
  • Loading branch information
JensRavens authored Oct 6, 2023
1 parent 1c2a4f6 commit d57a95f
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 78 deletions.
12 changes: 5 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@
"Jens Ravens"
],
"license": "MIT",
"dependencies": {
"react-dom": "^17.0.1"
},
"devDependencies": {
"react-dom": "^17.0.1",
"@babel/core": "^7.0.0-0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/plugin-transform-runtime": "^7.22.15",
"@babel/preset-env": "^7.16.11",
"@babel/preset-flow": "^7.16.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-flow": "^7.22.15",
"@babel/preset-react": "^7.22.15",
"@types/jest": "^27.0.3",
"@types/react": "^17.0.37",
"@typescript-eslint/eslint-plugin": "^5.6.0",
Expand Down Expand Up @@ -70,7 +68,7 @@
"node-forge": "0.10.0"
},
"peerDependencies": {
"react": "^16.13.1"
"react": ">16.13.1"
},
"repository": {
"type": "git",
Expand Down
4 changes: 3 additions & 1 deletion sandbox-app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { useForm } from '../src/index';
import { NestedField } from './nested-field';
import { NestedValidation } from './nested-validation';
import { NullableField } from './nullable-field';
import { ObjectsArray } from './objects-array';
import { SimpleArray } from './simple-array';
import { SimpleDelayedFields } from './simple-delayed-fields';
import { SimpleFields } from './simple-fields';
import { SimpleObject } from './simple-object';

const examples = [
'simple-fields',
'simple-delayed-fields',
'simple-array',
'objects-array',
'nested-field',
Expand All @@ -35,6 +36,7 @@ function App(): JSX.Element {
</ul>
<div>
<SimpleFields />
<SimpleDelayedFields />
<SimpleArray />
<ObjectsArray />
<NestedField />
Expand Down
58 changes: 58 additions & 0 deletions sandbox-app/simple-delayed-fields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useEffect } from 'react';
import { useForm } from '../src';
import { Input } from './components/Input';

function useMyApiValue(): string {
const [value, setValue] = React.useState('');
useEffect(() => {
setTimeout(() => setValue('John'), 1000);
}, []);
return value;
}

export function SimpleDelayedFields(): JSX.Element {
const name = useMyApiValue();
const {
model,
changes,
fields,
onSubmit,
valid,
dirty,
submissionStatus,
reset,
} = useForm({
model: { name },
onSubmit: async ({ model }) => {
// eslint-disable-next-line no-console
console.log(model);
},
validations: {
name: 'required',
},
});

return (
<>
<h2 id="simple-delayed-fields">Simple Delayed Fields</h2>
<form onSubmit={onSubmit}>
<div className="stack">
<Input label="Name: " {...fields.name} />
<footer>
<button>Submit</button>
<button onClick={reset} type="button">
Reset
</button>
</footer>
</div>
</form>
<h4>Model</h4>
<pre>
{JSON.stringify({ model, valid, dirty, submissionStatus }, null, 2)}
</pre>
<h4>Changes</h4>
<pre>{JSON.stringify({ changes }, null, 2)}</pre>
<a href="#">Back</a>
</>
);
}
19 changes: 19 additions & 0 deletions src/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,25 @@ export class FieldImplementation<T, Model>
this.#onUpdate();
};

updateOriginalValue(value: Partial<T>): void {
if (typeof value === 'object') {
this.#originalValue = { ...this.#originalValue, ...value };
Object.keys(value).forEach((key) => {
const field = this.fields[key];
if (!field.dirty) {
field.value = value[key];
this.value[key] = value[key];
}
field.updateOriginalValue(value[key]);
});
} else {
if (!this.dirty) {
this.value = value;
}
this.#originalValue = value;
}
}

onFocus: () => void = () => {
this.focused = true;
this.#onUpdate();
Expand Down
23 changes: 23 additions & 0 deletions src/form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,29 @@ describe(Form, () => {
form.fields.name.onChange('test');
expect(form.submissionStatus).toEqual('idle');
});

it('allows updating the underlying model', async () => {
const form = createForm({ value: { name: undefined } });
expect(form.model.name).toBeUndefined();
expect(form.fields.name.value).toBeUndefined();
expect(form.dirty).toEqual(false);
form.updateOriginalModel({ name: 'Jorge' });
expect(form.model.name).toEqual('Jorge');
expect(form.dirty).toEqual(false);
expect(form.fields.name.dirty).toEqual(false);
expect(form.fields.name.touched).toEqual(false);
expect(form.fields.name.value).toEqual('Jorge');
});

it('does not change the dirty status when changing the underlying model', async () => {
const form = createForm({ value: { name: 'Klaus' } });
form.fields.name.onChange('Claudia');
expect(form.fields.name.dirty).toEqual(true);
form.updateOriginalModel({ name: 'Jorge' });
expect(form.fields.name.dirty).toEqual(true);
expect(form.fields.name.value).toEqual('Claudia');
expect(form.model.name).toEqual('Claudia');
});
});

describe('fields array', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export class Form<T> {
return this.#field.value;
}

updateOriginalModel(value: Partial<T>): void {
this.#field.updateOriginalValue(value);
}

get dirty(): boolean {
return this.#field.dirty;
}
Expand Down
8 changes: 6 additions & 2 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useForceUpdate } from './util';
import { isEqual, useForceUpdate } from './util';
import { useRef } from 'react';
import { Form, SubmissionStatus } from './form';
import { MappedValidation } from './validation';
Expand Down Expand Up @@ -48,8 +48,12 @@ export function useForm<T>({
onInit,
})
);

const form = formRef.current;

if (!isEqual(form.model, model)) {
form.updateOriginalModel(model);
}

form.onSubmit = onSubmit;
form.onSubmitError = onSubmitError;

Expand Down
1 change: 1 addition & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function useForceUpdate(): () => void {
}

export function isEqual<T>(a: T, b: T): boolean {
if (a === b) return true; // faster in case there's actual equality
return JSON.stringify(a) === JSON.stringify(b);
}

Expand Down
Loading

0 comments on commit d57a95f

Please sign in to comment.