Repository: vazco/uniforms (ts)

Issue:
More complex onChange
Hi, we want to implement undo/redo feature. For that we need to know complete model on every change (accessible by `onChangeModel`). But we also need to know which key caused the change. Otherwise we would record every typed letter in an input, which is not how undo/redo should work. To avoid capturing subsequent changes in same field, we can use method `onChange` which provides `key` causing change with new `value`, but now we don't know whole model, so we cannot record it. Now we can only somehow combine key with current model, which causes troubles and bugs especially with nested fields.

Current solution, which I sense is also bug-prone and not quite clean is this:
```js

const handleFieldChange = (key, value) => {
        // This fires after all form.setState() is done
        // so we can catch last model, while knowing which key caused change
        formRef.current.setState(
                () => null,
                () => log("model change", key, value, formRef.current.state.model)
        );
};

return <AutoForm model={model} ref={formRef} onChange={handleFieldChange} />
```

So can we extend onModelChange feature to provide not only new model, but also key and value causing changes?
Thanks.

Pull request description:
Extend `onChangeModel` - add details about what changed
Closes #1359.

This PR extends `AutoForm.onChangeModel` to provide additional information `details: { key, value, previousValue }`.
- `key` is the changed key
- `value` is the new value
- `previousValue` is the value before the change

Patch:
diff --git a/docs/api-forms.md b/docs/api-forms.md
index 7cf3cf22..94c72842 100644
--- a/docs/api-forms.md
+++ b/docs/api-forms.md
@@ -25,9 +25,9 @@ By default, the validation will take place `onSubmit`, and `onChange` **after th
 
 ##### Props:
 
-|      Name       |                                                          Description                                                           |
-| :-------------: | :----------------------------------------------------------------------------------------------------------------------------: |
-| `onChangeModel` | Like `onChange` but for the whole model. Triggered just after `onChange` but with the next model instead of (key, value) pair. |
+|      Name       |                                                                                                              Description                                                                                                               |
+| :-------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| `onChangeModel` | Like `onChange` but for the whole model. Triggered just after `onChange` with the next model and information what `{ key, value, previousValue }` caused the change. `previousValue` will be `undefined` if there was no value before. |
 
 **Note:** All `ValidatedQuickForm` props are also accepted and all methods are available.
 In other words, that means that `AutoForm` receives all props listed on this page.
diff --git a/docs/migrating-3-to-4.md b/docs/migrating-3-to-4.md
index ef83a105..605561df 100644
--- a/docs/migrating-3-to-4.md
+++ b/docs/migrating-3-to-4.md
@@ -11,3 +11,4 @@ This guide is designed to help you through the migration. If you went through it
 - Dropped support for `initialCount` in bridges and `ListField`s. Pass a model object to the form with the appropriate amount of initial items instead.
 - `AutoFields` component in all themes now renders a `React.Fragment` instead of a `div`. Explicitly render a wrapper component around if you need one.
 - Removed the `autoField` prop from `QuickForm`, `AutoForm`, and `AutoFields` components in all themes. Use [`AutoField.componentDetectorContext.Provider`](/docs/uth-autofield-algorithm/#overriding-autofield) instead.
+- The constructors for all our bridges now accept an object (e.g., `{schema, validator}`) instead of individual parameters. This applies to `SimpleSchema2Bridge`, `JSONSchemaBridge`, and `ZodBridge`. Please update your constructor calls accordingly.
diff --git a/packages/uniforms/src/AutoForm.tsx b/packages/uniforms/src/AutoForm.tsx
index 0462b218..df47c844 100644
--- a/packages/uniforms/src/AutoForm.tsx
+++ b/packages/uniforms/src/AutoForm.tsx
@@ -1,4 +1,5 @@
 import clone from 'lodash/clone';
+import get from 'lodash/get';
 import isEqual from 'lodash/isEqual';
 import omit from 'lodash/omit';
 import setWith from 'lodash/setWith';
@@ -15,7 +16,10 @@ import { ModelTransformMode, UnknownObject } from './types';
 
 export type AutoFormProps<Model extends UnknownObject> =
   ValidatedQuickFormProps<Model> & {
-    onChangeModel?: (model: Model) => void;
+    onChangeModel?: (
+      model: Model,
+      details: { key: string; value: unknown; previousValue: unknown },
+    ) => void;
   };
 
 export type AutoFormState<Model extends UnknownObject> =
@@ -77,12 +81,17 @@ export function Auto<Base extends typeof ValidatedQuickForm>(Base: Base) {
     }
 
     onChange(key: string, value: unknown) {
+      const previousValue: unknown = get(this.state.model, key);
       super.onChange(key, value);
       this.setState(
         state => ({ model: setWith(clone(state.model), key, value, clone) }),
         () => {
           if (this.props.onChangeModel) {
-            this.props.onChangeModel(this.state.model);
+            this.props.onChangeModel(this.state.model, {
+              key,
+              value,
+              previousValue,
+            });
           }
         },
       );


Test patch:
diff --git a/packages/uniforms/__tests__/AutoForm.tsx b/packages/uniforms/__tests__/AutoForm.tsx
index 86355ba7..87fa53bb 100644
--- a/packages/uniforms/__tests__/AutoForm.tsx
+++ b/packages/uniforms/__tests__/AutoForm.tsx
@@ -67,7 +67,10 @@ describe('<AutoForm />', () => {
     });
 
     it('calls `onChangeModel`', () => {
-      const schema = new SimpleSchema({ a: { type: String, optional: true } });
+      const schema = new SimpleSchema({
+        a: { type: String, optional: true },
+        b: { type: String, optional: true },
+      });
       const bridge = new SimpleSchema2Bridge({ schema });
       render(
         <AutoForm onChangeModel={onChangeModel} schema={bridge}>
@@ -75,11 +78,27 @@ describe('<AutoForm />', () => {
         </AutoForm>,
       );
 
-      const input = screen.getByLabelText('A');
-      fireEvent.change(input, { target: { value: 'a' } });
+      const inputA = screen.getByLabelText('A');
+      fireEvent.change(inputA, { target: { value: 'a' } });
+      expect(onChangeModel).toHaveBeenLastCalledWith(
+        { a: 'a' },
+        { key: 'a', value: 'a', previousValue: undefined },
+      );
+
+      const inputB = screen.getByLabelText('B');
+      fireEvent.change(inputB, { target: { value: 'b' } });
+      expect(onChangeModel).toHaveBeenLastCalledWith(
+        { a: 'a', b: 'b' },
+        { key: 'b', value: 'b', previousValue: undefined },
+      );
+
+      fireEvent.change(inputB, { target: { value: 'bb' } });
+      expect(onChangeModel).toHaveBeenLastCalledWith(
+        { a: 'a', b: 'bb' },
+        { key: 'b', value: 'bb', previousValue: 'b' },
+      );
 
-      expect(onChangeModel).toHaveBeenCalledTimes(1);
-      expect(onChangeModel).toHaveBeenLastCalledWith({ a: 'a' });
+      expect(onChangeModel).toHaveBeenCalledTimes(3);
     });
 
     it('updates `changed` and `changedMap`', () => {