From 7bd4b4d0e9a27bc49bbc510972081c0a8a62281e Mon Sep 17 00:00:00 2001 From: Damien Clarke Date: Tue, 1 Mar 2022 11:55:31 +1100 Subject: [PATCH 1/4] feat: add syncHistory to replace sync --- packages/dendriform/src/Dendriform.tsx | 29 ++++- packages/dendriform/src/errors.ts | 4 +- packages/dendriform/src/index.ts | 1 + packages/dendriform/src/syncHistory.ts | 81 +++++++++++++ packages/dendriform/test/Dendriform.test.tsx | 114 ++++++++++++++++++- 5 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 packages/dendriform/src/syncHistory.ts diff --git a/packages/dendriform/src/Dendriform.tsx b/packages/dendriform/src/Dendriform.tsx index 9184686..eab59e7 100644 --- a/packages/dendriform/src/Dendriform.tsx +++ b/packages/dendriform/src/Dendriform.tsx @@ -109,10 +109,26 @@ export type DeriveCallbackRef = [string, DeriveCallback]; export type CancelCallback = (message: string) => void; +// +// chunks - allowing other files to register chunks of functionality for Dendriform to use +// + +export type ExecuteHistorySync = (changedFormSet: Set, offset?: number) => void; + +type ChunkRegistry = { + executeHistorySync?: ExecuteHistorySync; +}; + +export const _chunkRegistry: ChunkRegistry = {}; + + // // core // +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyCore = Core; + export type Plugins = {[key: string]: Plugin}|undefined; export type Options

= { @@ -194,7 +210,7 @@ export class Core { cancelCallbacks = new Set(); // set of forms currently hvaing their set() functions called // eslint-disable-next-line @typescript-eslint/no-explicit-any - static changingForms = new Set>(); + static changingForms = new Set(); // debounce ids and count numbers to identify when each id has debounced debounceMap = new Map(); @@ -357,12 +373,13 @@ export class Core { // call all change callbacks involved in this form and clear revert points if(originator) { + const changedForms = Array.from(Core.changingForms.values()); + // clear the changing forms set before calling callbacks // so any calls to .set in callbacks start a new change - const forms = Array.from(Core.changingForms.values()); this.finaliseChange(); - forms.forEach(form => form.callAllChangeCallbacks(internalMeta)); + changedForms.forEach(form => form.callAllChangeCallbacks(internalMeta)); } } catch(e) { @@ -458,6 +475,9 @@ export class Core { // e.g. when sync() deliberately adds empty history items if(historyItem.do.value.length > 0) { this.callAllDeriveCallbacks(internalMeta); + + // look through historySyncGroups to find if any forms need blank history items added + _chunkRegistry.executeHistorySync?.(Core.changingForms); } }, internalMeta); }; @@ -640,6 +660,9 @@ export class Core { // call derive callbacks this.callAllDeriveCallbacks(internalMeta); + + // look through historySyncGroups to find if any forms need their history updated + _chunkRegistry.executeHistorySync?.(Core.changingForms, offset); }, internalMeta); this.internalState.going = false; diff --git a/packages/dendriform/src/errors.ts b/packages/dendriform/src/errors.ts index e74fde9..0eb4fdf 100644 --- a/packages/dendriform/src/errors.ts +++ b/packages/dendriform/src/errors.ts @@ -10,7 +10,9 @@ const errors = { 6: (msg: string) => `onDerive() callback must not throw errors on first call. Threw: ${msg}`, 7: `Cannot call .set() on an element of an es6 Set`, 8: `Plugin must be passed into a Dendriform instance before this operation can be called`, - 9: `Cannot call .set() or .go() on a readonly form` + 9: `Cannot call .set() or .go() on a readonly form`, + 10: `All syncHistory() forms must each have a matching non-zero number of history items configured, e.g. {history: 10}`, + 11: `syncHistory() can only be applied to forms that have not had any changes made yet` } as const; export type ErrorKey = keyof typeof errors; diff --git a/packages/dendriform/src/index.ts b/packages/dendriform/src/index.ts index 83bd6ca..b94757f 100644 --- a/packages/dendriform/src/index.ts +++ b/packages/dendriform/src/index.ts @@ -7,6 +7,7 @@ export * from './useInput'; export * from './useCheckbox'; export * from './useProps'; export * from './sync'; +export * from './syncHistory'; export * from './diff'; export * from './LazyDerive'; export * from './Plugin'; diff --git a/packages/dendriform/src/syncHistory.ts b/packages/dendriform/src/syncHistory.ts new file mode 100644 index 0000000..bb66bf2 --- /dev/null +++ b/packages/dendriform/src/syncHistory.ts @@ -0,0 +1,81 @@ +import {useEffect} from 'react'; +import {noChange} from './producePatches'; +import {die} from './errors'; +import {_chunkRegistry} from './Dendriform'; +import type {Dendriform, AnyCore} from './Dendriform'; + +// +// history sync +// + +// state +// set of forms whose history is to be grouped together +const historySyncGroups = new Map(); +let historySyncGroupsNextKey = 0; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const historySync = (...forms: Dendriform[]): void => { + const cores = forms.map(form => form.core); + + // validate args + if(cores.length === 0 + || cores.some(core => core.historyLimit !== cores[0].historyLimit || core.historyLimit < 1) + ) { + die(10); + } + + if(cores.some(core => core.state.historyIndex > 0)) { + die(11); + } + + // create key + const thisKey = ++historySyncGroupsNextKey; + + // find existing keys in common with new ones + const commonKeys = new Set(); + cores.forEach(core => { + const existing = historySyncGroups.get(core); + if(existing) { + commonKeys.add(existing); + } + }); + + // update common keys to match newest key + Array.from(historySyncGroups.entries()).forEach(([core, key]) => { + if(commonKeys.has(key)) { + historySyncGroups.set(core, thisKey); + } + }); + + // add passed in keys + cores.forEach(core => { + historySyncGroups.set(core, thisKey); + }); +} + +export const useHistorySync = (...forms: Dendriform[]): void => { + useEffect(() => { + historySync(...forms); + }, []); +}; + +// look through historySyncGroups to find if any forms need blank history items added +// eslint-disable-next-line @typescript-eslint/no-explicit-any +_chunkRegistry.executeHistorySync = (changedFormSet: Set, offset: number = 0): void => { + + const changedSyncGroups = new Set( + Array.from(changedFormSet) + .map(core => historySyncGroups.get(core) as number) + .filter(key => !!key) + ); + + historySyncGroups.forEach((key, core) => { + if(!changedFormSet.has(core) && changedSyncGroups.has(key)) { + if(offset) { + core.go(offset); + } else { + core.set('0', noChange, {}); + } + } + }); +}; \ No newline at end of file diff --git a/packages/dendriform/test/Dendriform.test.tsx b/packages/dendriform/test/Dendriform.test.tsx index 1bd83cf..c0b8b87 100644 --- a/packages/dendriform/test/Dendriform.test.tsx +++ b/packages/dendriform/test/Dendriform.test.tsx @@ -1,4 +1,4 @@ -import {useDendriform, Dendriform, noChange, sync, useSync, immerable, cancel, Plugin} from '../src/index'; +import {useDendriform, Dendriform, historySync, useHistorySync, noChange, sync, useSync, immerable, cancel, Plugin} from '../src/index'; import {renderHook, act} from '@testing-library/react-hooks'; import {BASIC, OBJECT, ARRAY} from 'dendriform-immer-patch-optimiser'; @@ -533,22 +533,124 @@ describe(`Dendriform`, () => { const form = new Dendriform(123, {history: 100}); form.set(456); - // form.core.flush(); form.set(noChange); - // form.core.flush(); expect(form.value).toBe(456); form.undo(); - // form.core.flush(); expect(form.value).toBe(456); form.undo(); - // form.core.flush(); expect(form.value).toBe(123); }); + + describe(`historySync`, () => { + + test(`should allow forms to sync`, () => { + const formA = new Dendriform(123, {history: 100}); + const formB = new Dendriform(123, {history: 100}); + const formC = new Dendriform(123, {history: 100}); + + historySync(formA, formB); + + formA.set(456); + + expect(formA.core.state.historyIndex).toBe(1); + expect(formA.value).toBe(456); + expect(formB.core.state.historyIndex).toBe(1); + expect(formB.value).toBe(123); + expect(formC.core.state.historyIndex).toBe(0); + expect(formC.value).toBe(123); + + // undo with one form, expect other form to also undo + formA.undo(); + + expect(formA.core.state.historyIndex).toBe(0); + expect(formA.value).toBe(123); + expect(formB.core.state.historyIndex).toBe(0); + expect(formB.value).toBe(123); + expect(formC.core.state.historyIndex).toBe(0); + expect(formC.value).toBe(123); + + // redo with other form, expect other form to also redo + formB.redo(); + + expect(formA.core.state.historyIndex).toBe(1); + expect(formA.value).toBe(456); + expect(formB.core.state.historyIndex).toBe(1); + expect(formB.value).toBe(123); + expect(formC.core.state.historyIndex).toBe(0); + expect(formC.value).toBe(123); + }); + + test(`should join sync groups together`, () => { + const formA = new Dendriform(123, {history: 100}); + const formB = new Dendriform(123, {history: 100}); + const formC = new Dendriform(123, {history: 100}); + const formD = new Dendriform(123, {history: 100}); + const formE = new Dendriform(123, {history: 100}); + const formF = new Dendriform(123, {history: 100}); + + historySync(formA, formB); + historySync(formC, formD); + historySync(formC, formB); + historySync(formE, formF); + + formD.set(456); + + expect(formA.core.state.historyIndex).toBe(1); + expect(formB.core.state.historyIndex).toBe(1); + expect(formC.core.state.historyIndex).toBe(1); + expect(formD.core.state.historyIndex).toBe(1); + expect(formE.core.state.historyIndex).toBe(0); + expect(formF.core.state.historyIndex).toBe(0); + }); + + test(`should error if passed mismatched history sizes`, () => { + const formA = new Dendriform(123, {history: 100}); + const formB = new Dendriform(123, {history: 100}); + const formC = new Dendriform(123, {history: 200}); + + expect(() => historySync(formA, formB, formC)).toThrow('[Dendriform] All syncHistory() forms must each have a matching non-zero number of history items configured, e.g. {history: 10}'); + }); + + test(`should error if passed zero history sizes`, () => { + const formA = new Dendriform(123, {history: 100}); + const formB = new Dendriform(123); + + expect(() => historySync(formA, formB)).toThrow('[Dendriform] All syncHistory() forms must each have a matching non-zero number of history items configured, e.g. {history: 10}'); + }); + + test(`should error if any form has already changed`, () => { + const formA = new Dendriform(123, {history: 100}); + const formB = new Dendriform(123, {history: 100}); + const formC = new Dendriform(123, {history: 100}); + formC.set(456); + + expect(() => historySync(formA, formB, formC)).toThrow('[Dendriform] syncHistory() can only be applied to forms that have not had any changes made yet'); + }); + + test(`should useHistorySync`, () => { + + const hookA = renderHook(() => useDendriform(() => 'hi', {history: 10})); + const hookB = renderHook(() => useDendriform(() => 0, {history: 10})); + + const formA = hookA.result.current; + const formB = hookB.result.current; + + renderHook(() => { + useHistorySync(formA, formB); + }); + + act(() => { + formA.set('hello'); + }); + + expect(formB.core.state.historyIndex).toBe(1); + }); + }); }); describe('useDendriform() and prop changes', () => { @@ -2943,7 +3045,7 @@ describe(`Dendriform`, () => { }); }); - test(`should remove nodes as parent changes type`, () => { + test(`should remove nodes as parent changes type`, () => { const form = new Dendriform({ foo: { bar: 123, From 72fb45e0c72d6a9f5d88162da0d7cff6c5e4a6ff Mon Sep 17 00:00:00 2001 From: Damien Clarke Date: Tue, 1 Mar 2022 11:56:25 +1100 Subject: [PATCH 2/4] docs: add deprecation notice to sync --- packages/dendriform/src/sync.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dendriform/src/sync.ts b/packages/dendriform/src/sync.ts index 5ac1fc5..2e7bf0c 100644 --- a/packages/dendriform/src/sync.ts +++ b/packages/dendriform/src/sync.ts @@ -9,6 +9,8 @@ export const sync = ( derive?: DeriveCallback ): (() => void) => { + console.warn('sync() is deprecated and will be removed in the next major version. Use historySync() instead.'); + if(masterForm.core.historyLimit !== slaveForm.core.historyLimit) { die(5); } From 29482744daaa1b5834a2df315965eedc3da56446 Mon Sep 17 00:00:00 2001 From: Damien Clarke Date: Tue, 1 Mar 2022 11:56:40 +1100 Subject: [PATCH 3/4] feat: adjust size --- packages/dendriform/.size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dendriform/.size-limit.js b/packages/dendriform/.size-limit.js index f48126c..81a8d68 100644 --- a/packages/dendriform/.size-limit.js +++ b/packages/dendriform/.size-limit.js @@ -9,7 +9,7 @@ module.exports = [ name: 'Dendriform', path: "dist/dendriform.esm.js", import: "{ Dendriform }", - limit: "9.1 KB", + limit: "9.3 KB", ignore: ['react', 'react-dom'] } ]; From 03896d923ee8944114b8eee3f323f61f15ba39bc Mon Sep 17 00:00:00 2001 From: Damien Clarke Date: Tue, 1 Mar 2022 11:56:54 +1100 Subject: [PATCH 4/4] docs: update documentation --- README.md | 59 +++++---- packages/dendriform-demo/components/Demos.tsx | 118 +----------------- packages/dendriform/README.md | 59 +++++---- 3 files changed, 59 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index bacd286..1fac113 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Advanced usage - [Diffing changes](#diffing-changes) - [Deriving data](#deriving-data) -- [Synchronising forms](#synchronising-forms) +- [Synchronising form history](#synchronising-form-history) - [Cancel changes based on constraints](#cancel-changes-based-on-constraints) - [Lazy derive](#lazy-derive) @@ -1511,41 +1511,57 @@ stringForm.set('30'); [Demo](http://dendriform.xyz#derivetwoway) -### Synchronising forms +### Synchronising form history -**Warning:** *the {sync} API is experimental and may be replaced or removed in future.* +You can use any number of forms to store your editable state so you can keep related data grouped logically together. However you might also want several separate forms to move through history together, so calling `.undo()` on one will also undo the changes that have occurred in multiple forms. The `syncHistory` utility can do this. -You can use any number of forms to store your editable state so you can keep related data grouped logically together. However you might also want several separate forms to move through history together, so calling `.undo()` will undo the changes that have occurred in multiple forms. The `sync` utility can do this. - -Synchronised forms must have the same maximum number of history items configured. +Synchronised forms must have the same maximum number of history items configured, and `syncHistory` must be called before any of the affected forms have any changes made to them. ```js -import {sync} from 'dendriform'; +import {syncHistory} from 'dendriform'; const nameForm = new Dendriform({name: 'Bill'}, {history: 100}); const addressForm = new Dendriform({street: 'Cool St'}, {history: 100}); -const unsync = sync(nameForm, addressForm); +syncHistory(nameForm, addressForm); // if nameForm.undo() is called, addressForm.undo() is also called, and vice versa // if nameForm.redo() is called, addressForm.redo() is also called, and vice versa // if nameForm.go() is called, addressForm.go() is also called, and vice versa +``` + +Multiple forms can be synced together through multiple calls fo `syncHistory`, so it's possible to append forms to a groups of already-synchronised forms. + +```js +import {syncHistory} from 'dendriform'; + +const nameForm = new Dendriform('Noof', {history: 100}); +const addressForm = new Dendriform('12 Foo St', {history: 100}); +const suburbForm = new Dendriform('Suburbtown', {history: 100}); + +syncHistory(nameForm, addressForm, suburbForm); + +// later, but before any changes are made to any affected forms, other forms can be synchronised + +const colourForm = new Dendriform('Blue', {history: 100}); + +syncHistory(suburbForm, colourForm); -// call unsync() to unsynchronise the forms +// now nameForm, addressForm, suburbForm and colourForm will be synchronised through history ``` [Demo](http://dendriform.xyz#sync) -Inside of a React component you can use the `useSync()` hook to achieve the same result. +Inside of a React component you can use the `useHistorySync()` hook to achieve the same result. ```js -import {useSync} from 'dendriform'; +import {useHistorySync} from 'dendriform'; function MyComponent(props) { const personForm = useDendriform(() => ({name: 'Bill'}), {history: 100}); const addressForm = useDendriform(() => ({street: 'Cool St'}), {history: 100}); - useSync(personForm, addressForm); + useHistorySync(personForm, addressForm); return

{personForm.render('name', nameForm => ( @@ -1567,25 +1583,6 @@ function MyComponent(props) { } ``` -The `sync()` function can also accept a deriver to derive data in one direction. This has the effect of caching each derived form state in history, and calling undo and redo will just restore the relevant derived data at that point in history. - -```js -import {sync} from 'dendriform'; - -const namesForm = new Dendriform(['Bill', 'Ben', 'Bob'], {history: 100}); - -const addressForm = new Dendriform({ - street: 'Cool St', - occupants: 0 -}, {history: 100}); - -sync(nameForm, addressForm, names => { - addressForm.branch('occupants').set(names.length); -}); -``` - -[Demo](http://dendriform.xyz#syncderive) - ### Cancel changes based on constraints Forms can have constraints applied to prevent invalid data from being set. An `.onDerive()` / `.useDerive()` callback may optionally throw a `cancel()` to cancel and revert the change that is being currently applied. diff --git a/packages/dendriform-demo/components/Demos.tsx b/packages/dendriform-demo/components/Demos.tsx index 4679099..f097543 100644 --- a/packages/dendriform-demo/components/Demos.tsx +++ b/packages/dendriform-demo/components/Demos.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useState, useRef, memo} from 'react'; -import {Dendriform, useDendriform, useInput, useCheckbox, useSync, array, immerable, cancel, diff, PluginSubmit} from 'dendriform'; +import {Dendriform, useDendriform, useInput, useCheckbox, useHistorySync, array, immerable, cancel, diff, PluginSubmit} from 'dendriform'; import {Box, Flex} from '../components/Layout'; import {H2, Link, Text} from '../components/Text'; import styled from 'styled-components'; @@ -1334,7 +1334,7 @@ function Sync(): React.ReactElement { const nameForm = useDendriform(() => ({name: 'Bill'}), {history: 100}); const addressForm = useDendriform(() => ({street: 'Cool St'}), {history: 100}); - useSync(nameForm, addressForm); + useHistorySync(nameForm, addressForm); return {nameForm.render('name', nameForm => ( @@ -1360,7 +1360,7 @@ function MyComponent(props) { const nameForm = useDendriform(() => ({name: 'Bill'}), {history: 100}); const addressForm = useDendriform(() => ({street: 'Cool St'}), {history: 100}); - useSync(nameForm, addressForm); + useHistorySync(nameForm, addressForm); return
{nameForm.render('name', nameForm => ( @@ -1382,110 +1382,6 @@ function MyComponent(props) { } `; -// -// sync derive -// - -function SyncDerive(): React.ReactElement { - - const namesForm = useDendriform(() => ['Bill', 'Ben', 'Bob'], {history: 100}); - - const addressForm = useDendriform(() => ({ - street: 'Cool St', - occupants: 0 - }), {history: 100}); - - useSync(namesForm, addressForm, names => { - // eslint-disable-next-line no-console - console.log(`Deriving occupants for ${JSON.stringify(names)}`); - addressForm.branch('occupants').set(names.length); - }); - - const addName = useCallback(() => { - namesForm.set(draft => { - draft.push('Name ' + draft.length); - }); - }, []); - - return -
- names -
    - {namesForm.renderAll(nameForm => - - )} -
- -
- - {addressForm.render('street', streetForm => ( - street: - ))} - - {addressForm.render('occupants', occupantsForm => ( - occupants: {occupantsForm.useValue()} - ))} - - {namesForm.render(namesForm => { - const {canUndo, canRedo} = namesForm.useHistory(); - return - - - ; - })} -
; -} - -const SyncDeriveCode = ` -function MyComponent(props) { - - const namesForm = useDendriform(() => ['Bill', 'Ben', 'Bob'], {history: 100}); - - const addressForm = useDendriform(() => ({ - street: 'Cool St', - occupants: 0 - }), {history: 100}); - - useSync(namesForm, addressForm, names => { - addressForm.branch('occupants').set(names.length); - }); - - const addName = useCallback(() => { - namesForm.set(draft => { - draft.push('Name ' + draft.length); - }); - }, []); - - return
-
- names -
    - {namesForm.renderAll(nameForm => - - )} -
- -
- - {addressForm.render('street', streetForm => ( - - ))} - - {addressForm.render('occupants', occupantsForm => ( - occupants: {occupantsForm.useValue()} - ))} - - {namesForm.render(namesForm => { - const {canUndo, canRedo} = namesForm.useHistory(); - return <> - - - ; - })} -
; -} -`; - // // drag and drop // @@ -2832,14 +2728,6 @@ const ADVANCED_DEMOS: DemoObject[] = [ anchor: 'sync', more: 'synchronising-forms' }, - { - title: 'Synchronising forms with deriving', - Demo: SyncDerive, - code: SyncDeriveCode, - description: `The useSync() hook can also accept a deriver to derive data in one direction. This has the effect of caching each derived form state in history, and calling undo and redo will just restore the relevant derived data at that point in history.`, - anchor: 'syncderive', - more: 'synchronising-forms' - }, { title: 'Cancel changes based on constraints', Demo: Cancel, diff --git a/packages/dendriform/README.md b/packages/dendriform/README.md index bacd286..1fac113 100644 --- a/packages/dendriform/README.md +++ b/packages/dendriform/README.md @@ -162,7 +162,7 @@ Advanced usage - [Diffing changes](#diffing-changes) - [Deriving data](#deriving-data) -- [Synchronising forms](#synchronising-forms) +- [Synchronising form history](#synchronising-form-history) - [Cancel changes based on constraints](#cancel-changes-based-on-constraints) - [Lazy derive](#lazy-derive) @@ -1511,41 +1511,57 @@ stringForm.set('30'); [Demo](http://dendriform.xyz#derivetwoway) -### Synchronising forms +### Synchronising form history -**Warning:** *the {sync} API is experimental and may be replaced or removed in future.* +You can use any number of forms to store your editable state so you can keep related data grouped logically together. However you might also want several separate forms to move through history together, so calling `.undo()` on one will also undo the changes that have occurred in multiple forms. The `syncHistory` utility can do this. -You can use any number of forms to store your editable state so you can keep related data grouped logically together. However you might also want several separate forms to move through history together, so calling `.undo()` will undo the changes that have occurred in multiple forms. The `sync` utility can do this. - -Synchronised forms must have the same maximum number of history items configured. +Synchronised forms must have the same maximum number of history items configured, and `syncHistory` must be called before any of the affected forms have any changes made to them. ```js -import {sync} from 'dendriform'; +import {syncHistory} from 'dendriform'; const nameForm = new Dendriform({name: 'Bill'}, {history: 100}); const addressForm = new Dendriform({street: 'Cool St'}, {history: 100}); -const unsync = sync(nameForm, addressForm); +syncHistory(nameForm, addressForm); // if nameForm.undo() is called, addressForm.undo() is also called, and vice versa // if nameForm.redo() is called, addressForm.redo() is also called, and vice versa // if nameForm.go() is called, addressForm.go() is also called, and vice versa +``` + +Multiple forms can be synced together through multiple calls fo `syncHistory`, so it's possible to append forms to a groups of already-synchronised forms. + +```js +import {syncHistory} from 'dendriform'; + +const nameForm = new Dendriform('Noof', {history: 100}); +const addressForm = new Dendriform('12 Foo St', {history: 100}); +const suburbForm = new Dendriform('Suburbtown', {history: 100}); + +syncHistory(nameForm, addressForm, suburbForm); + +// later, but before any changes are made to any affected forms, other forms can be synchronised + +const colourForm = new Dendriform('Blue', {history: 100}); + +syncHistory(suburbForm, colourForm); -// call unsync() to unsynchronise the forms +// now nameForm, addressForm, suburbForm and colourForm will be synchronised through history ``` [Demo](http://dendriform.xyz#sync) -Inside of a React component you can use the `useSync()` hook to achieve the same result. +Inside of a React component you can use the `useHistorySync()` hook to achieve the same result. ```js -import {useSync} from 'dendriform'; +import {useHistorySync} from 'dendriform'; function MyComponent(props) { const personForm = useDendriform(() => ({name: 'Bill'}), {history: 100}); const addressForm = useDendriform(() => ({street: 'Cool St'}), {history: 100}); - useSync(personForm, addressForm); + useHistorySync(personForm, addressForm); return
{personForm.render('name', nameForm => ( @@ -1567,25 +1583,6 @@ function MyComponent(props) { } ``` -The `sync()` function can also accept a deriver to derive data in one direction. This has the effect of caching each derived form state in history, and calling undo and redo will just restore the relevant derived data at that point in history. - -```js -import {sync} from 'dendriform'; - -const namesForm = new Dendriform(['Bill', 'Ben', 'Bob'], {history: 100}); - -const addressForm = new Dendriform({ - street: 'Cool St', - occupants: 0 -}, {history: 100}); - -sync(nameForm, addressForm, names => { - addressForm.branch('occupants').set(names.length); -}); -``` - -[Demo](http://dendriform.xyz#syncderive) - ### Cancel changes based on constraints Forms can have constraints applied to prevent invalid data from being set. An `.onDerive()` / `.useDerive()` callback may optionally throw a `cancel()` to cancel and revert the change that is being currently applied.