diff --git a/README.md b/README.md index 10beaad..3f85ca8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Build feature-rich data-editing React UIs with great performance and little code **[See the demos](http://dendriform.xyz)** -*Available on npm only as pre-release for now. All docs refer to the upcoming version 2.0.0.* +[*Available on npm*](https://www.npmjs.com/package/dendriform) ```js import React, {useCallback} from 'react'; @@ -129,9 +129,13 @@ npm install --save dendriform - [Subscribing to changes](#subscribing-to-changes) - [Array operations](#array-operations) - [History](#history) -- [Synchronising forms](#synchronising-forms) - [Drag and drop](#drag-and-drop) +Advanced usage + +- [Deriving data](#deriving-data) +- [Synchronising forms](#synchronising-forms) + ### Creation Create a new dendriform form using `new Dendriform()`, or by using the `useDendriform()` hook if you're inside a React component's render method. Pass it the initial value to put in the form, or a function that returns your initial value. @@ -524,33 +528,6 @@ function MyComponent(props) { } ``` -#### options.track (advanced users) - -Dendriform automatically assigns a unique id to every nested property and array element of the data shape it contains. It uses these ids to track the movement of array elements over time, and uniquely keys any rendered React elements. By default a call to `.set()` will analyse the resulting data shape and track how any array elements may have moved by identifying each element with strict equality checks. - -However you may want to disable this tracking temporarily, for example if you want to replace an array with another whose elements maybe be equal by value but are not strictly equal. This can occur sometimes when setting a form's value based on props that have not been memoised properly. - -```js -const form = new Dendriform([{foo: 123}, {foo: 456}]); - -// form.branch(0).id is '1' -// form.branch(1).id is '2' - -form.set([{foo: 123}, {foo: 456}]); -// ^ these 2 new objects are not recognised as they are not strictly equal -// to the previous objects, so these will be given new ids - -// form.branch(0).id is '3' -// form.branch(1).id is '4' - -form.set([{foo: 123}, {foo: 456}], {track: false}); -// ^ with track: false, Dendriform will not attempt to track array element movement -// so the ids for each element will remain as they are - -// form.branch(0).id is '3' -// form.branch(1).id is '4' -``` - #### Buffering To call it multiple times in a row, use `buffer()` to begin buffering changes and `done()` to apply the changes. These will affect the entire form including all branches, so `form.buffer()` has the same effect as `form.branch('example').buffer()`. @@ -1046,6 +1023,79 @@ form.done(); [Demo](http://dendriform.xyz#historygroup) +### Drag and drop + +Drag and drop can be implemented easily with libraries such as [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd), because dendriform takes care of the unique keying of array elements for you. + +```js +import {DragDropContext, Droppable, Draggable} from 'react-beautiful-dnd'; + +const dndReorder = (result) => (draft) => { + if(!result.destination) return; + + const startIndex = result.source.index; + const endIndex = result.destination.index; + if(endIndex === startIndex) return; + + const [removed] = draft.splice(startIndex, 1); + draft.splice(endIndex, 0, removed); +}; + +function DragAndDrop() { + + const form = useDendriform({ + colours: ['Red', 'Green', 'Blue'] + }); + + const onDragEnd = useCallback(result => { + form.branch('colours').set(dndReorder(result)); + }, []); + + const onAdd = useCallback(() => { + form.branch('colours').set(array.push('Puce')); + }, []); + + return
+ + + {provided => ( +
+ + {provided.placeholder} +
+ )} +
+
+ + +
; +} + +function DragAndDropList(props) { + return props.form.renderAll(form => { + + const id = \`$\{form.id}\`; + const index = form.useIndex(); + const remove = useCallback(() => form.set(array.remove()), []); + + return + {provided =>
+ + +
} +
; + }); +} +``` + +[Demo](http://dendriform.xyz#draganddrop) + +## Advanced usage + ### Deriving data When a change occurs, you can derive additional data in your form using `.onDerive`, or by using the `.useDerive()` hook if you're inside a React component's render method. Each derive function is called once immediately, and then once per change after that. When a change occurs, all derive callbacks are called in the order they were attached, after which `.onChange()`, `.useChange()` and `.useValue()` are updated with the final value. @@ -1178,77 +1228,6 @@ sync(nameForm, addressForm, names => { [Demo](http://dendriform.xyz#syncderive) -## Drag and drop - -Drag and drop can be implemented easily with libraries such as [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd), because dendriform takes care of the unique keying of array elements for you. - -```js -import {DragDropContext, Droppable, Draggable} from 'react-beautiful-dnd'; - -const dndReorder = (result) => (draft) => { - if(!result.destination) return; - - const startIndex = result.source.index; - const endIndex = result.destination.index; - if(endIndex === startIndex) return; - - const [removed] = draft.splice(startIndex, 1); - draft.splice(endIndex, 0, removed); -}; - -function DragAndDrop() { - - const form = useDendriform({ - colours: ['Red', 'Green', 'Blue'] - }); - - const onDragEnd = useCallback(result => { - form.branch('colours').set(dndReorder(result)); - }, []); - - const onAdd = useCallback(() => { - form.branch('colours').set(array.push('Puce')); - }, []); - - return
- - - {provided => ( -
- - {provided.placeholder} -
- )} -
-
- - -
; -} - -function DragAndDropList(props) { - return props.form.renderAll(form => { - - const id = \`$\{form.id}\`; - const index = form.useIndex(); - const remove = useCallback(() => form.set(array.remove()), []); - - return - {provided =>
- - -
} -
; - }); -} -``` - -[Demo](http://dendriform.xyz#draganddrop) - ## Etymology "Dendriform" means "tree shaped", referencing the tree-like manner in which you can traverse and render the parts of a deep data shape. diff --git a/packages/dendriform-demo/components/Demos.tsx b/packages/dendriform-demo/components/Demos.tsx index 9ab8804..e602a50 100644 --- a/packages/dendriform-demo/components/Demos.tsx +++ b/packages/dendriform-demo/components/Demos.tsx @@ -1808,6 +1808,107 @@ function MyComponent(props) { } `; +// +// set() track +// + +function SetTrack(): React.ReactElement { + + const form = useDendriform(() => ({ + pets: [ + {name: 'Spike'}, + {name: 'Spoke'} + ] + })); + + const addPet = useCallback(() => { + form.branch('pets').set(draft => { + draft.push({name: 'new pet'}); + }); + }, []); + + const reverseStrict = useCallback(() => { + const value = JSON.parse(JSON.stringify(form.value)); + form.set(draft => { + draft.pets.reverse(); + }); + }, []); + + const reverseValue = useCallback(() => { + form.set(draft => { + // deliberately create new object references + draft.pets = JSON.parse(JSON.stringify(draft.pets)); + draft.pets.reverse(); + }); + }, []); + + return +
+ pets + + + + +
+ + +
; +} + +const SetTrackCode = ` +import React, {useCallback} from 'react'; +import {useDendriform, useInput} from 'dendriform'; + +function MyComponent(props) { + + const form = useDendriform(() => ({ + pets: [ + {name: 'Spike'}, + {name: 'Spoke'} + ] + }); + + const reverseStrict = useCallback(() => { + const value = JSON.parse(JSON.stringify(form.value)); + form.set(draft => { + draft.pets.reverse(); + }); + }, []); + + const reverseValue = useCallback(() => { + form.set(draft => { + // deliberately create new object references + draft.pets = JSON.parse(JSON.stringify(draft.pets)); + draft.pets.reverse(); + }); + }, []); + + return
+
+ pets + + +
+ + +
; +}; +`; + // // region // @@ -1968,6 +2069,13 @@ const DEMOS: DemoObject[] = [ anchor: 'indexes', more: 'array-operations' }, + { + title: 'Array element tracking', + Demo: SetTrack, + code: SetTrackCode, + description: `This demonstrates how Dendriform uses strict equality to track array element movement over time, which it uses for React element keying.`, + anchor: 'set-track' + }, { title: 'History', Demo: History, @@ -1984,6 +2092,24 @@ const DEMOS: DemoObject[] = [ anchor: 'historygroup', more: 'history' }, + { + title: 'Drag and drop with react-beautiful-dnd', + Demo: DragAndDrop, + code: DragAndDropCode, + 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 ADVANCED_DEMOS: DemoObject[] = [ + { + title: 'Validation example', + Demo: Validation, + code: ValidationCode, + description: `An example of how it's possible to perform validation on an array of items.`, + anchor: 'validation' + }, { title: 'Deriving data in a single form', Demo: Deriving, @@ -2016,14 +2142,6 @@ const DEMOS: DemoObject[] = [ anchor: 'syncderive', more: 'synchronising-forms' }, - { - title: 'Drag and drop with react-beautiful-dnd', - Demo: DragAndDrop, - code: DragAndDropCode, - 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' - }, // { // title: 'Keeping track of input refs', // Demo: InputRefs, @@ -2031,13 +2149,6 @@ const DEMOS: DemoObject[] = [ // description: `It's possible to use Dendriform forms to store refs to inputs being rendered. This allows you to access the refs from outside of the local component instances, which is particularly useful for focus management.`, // anchor: 'refs' // }, - { - title: 'Validation example', - Demo: Validation, - code: ValidationCode, - description: `An example of how it's possible to perform validation on an array of items.`, - anchor: 'validation' - } ]; export function Demos(): React.ReactElement { @@ -2046,6 +2157,12 @@ export function Demos(): React.ReactElement { ; } +export function AdvancedDemos(): React.ReactElement { + return + {ADVANCED_DEMOS.map(demo => )} + ; +} + type DemoProps = { demo: DemoObject }; diff --git a/packages/dendriform-demo/pages/index.tsx b/packages/dendriform-demo/pages/index.tsx index c8994c8..de99d9a 100644 --- a/packages/dendriform-demo/pages/index.tsx +++ b/packages/dendriform-demo/pages/index.tsx @@ -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} from '../components/Demos'; +import {Demos, AdvancedDemos} from '../components/Demos'; import type {ThemeProps} from '../pages/_app'; export default function Main(): React.ReactElement { @@ -32,9 +32,16 @@ export default function Main(): React.ReactElement { White flashes indicate regions of the page that React has re-rendered. You can see how performant Dendriform's rendering is by how localised these flashes are. - + +
+ +

Advanced Demos

+
+ + + ; } diff --git a/packages/dendriform/README.md b/packages/dendriform/README.md index 10beaad..3f85ca8 100644 --- a/packages/dendriform/README.md +++ b/packages/dendriform/README.md @@ -11,7 +11,7 @@ Build feature-rich data-editing React UIs with great performance and little code **[See the demos](http://dendriform.xyz)** -*Available on npm only as pre-release for now. All docs refer to the upcoming version 2.0.0.* +[*Available on npm*](https://www.npmjs.com/package/dendriform) ```js import React, {useCallback} from 'react'; @@ -129,9 +129,13 @@ npm install --save dendriform - [Subscribing to changes](#subscribing-to-changes) - [Array operations](#array-operations) - [History](#history) -- [Synchronising forms](#synchronising-forms) - [Drag and drop](#drag-and-drop) +Advanced usage + +- [Deriving data](#deriving-data) +- [Synchronising forms](#synchronising-forms) + ### Creation Create a new dendriform form using `new Dendriform()`, or by using the `useDendriform()` hook if you're inside a React component's render method. Pass it the initial value to put in the form, or a function that returns your initial value. @@ -524,33 +528,6 @@ function MyComponent(props) { } ``` -#### options.track (advanced users) - -Dendriform automatically assigns a unique id to every nested property and array element of the data shape it contains. It uses these ids to track the movement of array elements over time, and uniquely keys any rendered React elements. By default a call to `.set()` will analyse the resulting data shape and track how any array elements may have moved by identifying each element with strict equality checks. - -However you may want to disable this tracking temporarily, for example if you want to replace an array with another whose elements maybe be equal by value but are not strictly equal. This can occur sometimes when setting a form's value based on props that have not been memoised properly. - -```js -const form = new Dendriform([{foo: 123}, {foo: 456}]); - -// form.branch(0).id is '1' -// form.branch(1).id is '2' - -form.set([{foo: 123}, {foo: 456}]); -// ^ these 2 new objects are not recognised as they are not strictly equal -// to the previous objects, so these will be given new ids - -// form.branch(0).id is '3' -// form.branch(1).id is '4' - -form.set([{foo: 123}, {foo: 456}], {track: false}); -// ^ with track: false, Dendriform will not attempt to track array element movement -// so the ids for each element will remain as they are - -// form.branch(0).id is '3' -// form.branch(1).id is '4' -``` - #### Buffering To call it multiple times in a row, use `buffer()` to begin buffering changes and `done()` to apply the changes. These will affect the entire form including all branches, so `form.buffer()` has the same effect as `form.branch('example').buffer()`. @@ -1046,6 +1023,79 @@ form.done(); [Demo](http://dendriform.xyz#historygroup) +### Drag and drop + +Drag and drop can be implemented easily with libraries such as [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd), because dendriform takes care of the unique keying of array elements for you. + +```js +import {DragDropContext, Droppable, Draggable} from 'react-beautiful-dnd'; + +const dndReorder = (result) => (draft) => { + if(!result.destination) return; + + const startIndex = result.source.index; + const endIndex = result.destination.index; + if(endIndex === startIndex) return; + + const [removed] = draft.splice(startIndex, 1); + draft.splice(endIndex, 0, removed); +}; + +function DragAndDrop() { + + const form = useDendriform({ + colours: ['Red', 'Green', 'Blue'] + }); + + const onDragEnd = useCallback(result => { + form.branch('colours').set(dndReorder(result)); + }, []); + + const onAdd = useCallback(() => { + form.branch('colours').set(array.push('Puce')); + }, []); + + return
+ + + {provided => ( +
+ + {provided.placeholder} +
+ )} +
+
+ + +
; +} + +function DragAndDropList(props) { + return props.form.renderAll(form => { + + const id = \`$\{form.id}\`; + const index = form.useIndex(); + const remove = useCallback(() => form.set(array.remove()), []); + + return + {provided =>
+ + +
} +
; + }); +} +``` + +[Demo](http://dendriform.xyz#draganddrop) + +## Advanced usage + ### Deriving data When a change occurs, you can derive additional data in your form using `.onDerive`, or by using the `.useDerive()` hook if you're inside a React component's render method. Each derive function is called once immediately, and then once per change after that. When a change occurs, all derive callbacks are called in the order they were attached, after which `.onChange()`, `.useChange()` and `.useValue()` are updated with the final value. @@ -1178,77 +1228,6 @@ sync(nameForm, addressForm, names => { [Demo](http://dendriform.xyz#syncderive) -## Drag and drop - -Drag and drop can be implemented easily with libraries such as [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd), because dendriform takes care of the unique keying of array elements for you. - -```js -import {DragDropContext, Droppable, Draggable} from 'react-beautiful-dnd'; - -const dndReorder = (result) => (draft) => { - if(!result.destination) return; - - const startIndex = result.source.index; - const endIndex = result.destination.index; - if(endIndex === startIndex) return; - - const [removed] = draft.splice(startIndex, 1); - draft.splice(endIndex, 0, removed); -}; - -function DragAndDrop() { - - const form = useDendriform({ - colours: ['Red', 'Green', 'Blue'] - }); - - const onDragEnd = useCallback(result => { - form.branch('colours').set(dndReorder(result)); - }, []); - - const onAdd = useCallback(() => { - form.branch('colours').set(array.push('Puce')); - }, []); - - return
- - - {provided => ( -
- - {provided.placeholder} -
- )} -
-
- - -
; -} - -function DragAndDropList(props) { - return props.form.renderAll(form => { - - const id = \`$\{form.id}\`; - const index = form.useIndex(); - const remove = useCallback(() => form.set(array.remove()), []); - - return - {provided =>
- - -
} -
; - }); -} -``` - -[Demo](http://dendriform.xyz#draganddrop) - ## Etymology "Dendriform" means "tree shaped", referencing the tree-like manner in which you can traverse and render the parts of a deep data shape.