Skip to content

Commit

Permalink
Merge pull request #83 from dxinteractive/feature/sync-groups
Browse files Browse the repository at this point in the history
historySync()
  • Loading branch information
dxinteractive authored Mar 1, 2022
2 parents 5265004 + 03896d9 commit 2f04a7c
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 188 deletions.
59 changes: 28 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 <div>
{personForm.render('name', nameForm => (
Expand All @@ -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.
Expand Down
118 changes: 3 additions & 115 deletions packages/dendriform-demo/components/Demos.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 <Region>
{nameForm.render('name', nameForm => (
Expand All @@ -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 <div>
{nameForm.render('name', nameForm => (
Expand All @@ -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 <Region>
<fieldset>
<legend>names</legend>
<ul>
{namesForm.renderAll(nameForm => <Region of="li">
<label><input {...useInput(nameForm, 150)} /></label>
</Region>)}
</ul>
<button type="button" onClick={addName}>Add name</button>
</fieldset>

{addressForm.render('street', streetForm => (
<Region of="label">street: <input {...useInput(streetForm, 150)} /></Region>
))}

{addressForm.render('occupants', occupantsForm => (
<Region of="code">occupants: {occupantsForm.useValue()}</Region>
))}

{namesForm.render(namesForm => {
const {canUndo, canRedo} = namesForm.useHistory();
return <Region>
<button type="button" onClick={namesForm.undo} disabled={!canUndo}>Undo</button>
<button type="button" onClick={namesForm.redo} disabled={!canRedo}>Redo</button>
</Region>;
})}
</Region>;
}

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 <div>
<fieldset>
<legend>names</legend>
<ul>
{namesForm.renderAll(nameForm => <Region of="li">
<label><input {...useInput(nameForm, 150)} /></label>
</Region>)}
</ul>
<button type="button" onClick={addName}>Add name</button>
</fieldset>
{addressForm.render('street', streetForm => (
<label>street: <input {...useInput(streetForm, 150)} /></label>
))}
{addressForm.render('occupants', occupantsForm => (
<code>occupants: {occupantsForm.useValue()}</code>
))}
{namesForm.render(namesForm => {
const {canUndo, canRedo} = namesForm.useHistory();
return <>
<button type="button" onClick={namesForm.undo} disabled={!canUndo}>Undo</button>
<button type="button" onClick={namesForm.redo} disabled={!canRedo}>Redo</button>
</>;
})}
</div>;
}
`;

//
// drag and drop
//
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/dendriform/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
];
59 changes: 28 additions & 31 deletions packages/dendriform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 <div>
{personForm.render('name', nameForm => (
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 2f04a7c

Please sign in to comment.