From ca1dce72e61482a0a3b7a0419ad10abc3f77fd86 Mon Sep 17 00:00:00 2001 From: Damien Clarke Date: Tue, 21 May 2019 00:19:48 +1000 Subject: [PATCH] wip: wip --- README.md | 2 +- packages/dataparcels-docs/package.json | 1 - .../src/component/APINavigation.jsx | 8 +- .../src/component/exampleFrame.jsx | 70 ++ packages/dataparcels-docs/src/content/API.js | 34 +- .../docs/api/parcelBoundary/ParcelBoundary.md | 17 +- ...{modifyBeforeUpdate.md => beforeChange.md} | 12 +- .../docs/api/parcelBoundary/childRenderer.md | 27 +- .../docs/api/parcelBoundary/debugBuffer.md | 5 - .../docs/api/parcelBoundary/debugParcel.md | 5 - .../docs/api/parcelBoundary/forceUpdate.md | 4 +- .../src/docs/api/parcelBoundary/hold.md | 2 - .../src/docs/api/parcelBoundary/keepValue.md | 14 +- .../src/docs/api/parcelBoundary/onCancel.md | 18 - .../src/docs/api/parcelBoundary/onRelease.md | 18 - .../src/docs/api/parcelHoc/ParcelHoc.md | 3 - .../src/docs/api/parcelHoc/delayUntil.md | 2 - .../src/docs/api/parcelHoc/name.md | 2 - .../src/docs/api/parcelHoc/onChange.md | 2 - .../parcelHoc/shouldParcelUpdateFromProps.md | 2 - .../src/docs/api/parcelHoc/valueFromProps.md | 2 - .../src/docs/api/validation/Validation.md | 9 +- .../docs/api/validation/ValidationResult.md | 26 +- .../src/examples/Autosave.jsx | 46 +- .../src/examples/DerivedMeta.jsx | 48 +- .../src/examples/DerivedValue.jsx | 37 +- .../src/examples/EditingArrays.jsx | 31 +- .../src/examples/EditingArraysDrag.jsx | 33 +- .../src/examples/EditingArraysFlipMove.jsx | 34 - .../examples/EditingModifyAlphanumeric.jsx | 29 +- .../src/examples/EditingModifyDelimited.jsx | 29 +- .../src/examples/EditingModifyMissing.jsx | 31 +- .../src/examples/EditingModifyNumber.jsx | 35 +- .../src/examples/EditingObjects.jsx | 60 +- .../src/examples/EditingObjectsBeginner.jsx | 49 +- .../src/examples/InteractingFields.jsx | 41 +- .../src/examples/ManagingOwnParcelState.jsx | 47 +- .../src/examples/ModifyConditional.jsx | 35 - .../src/examples/ParcelBoundaryDebounce.jsx | 29 +- .../examples/ParcelBoundaryForceUpdate.jsx | 45 -- .../src/examples/ParcelBoundaryHold.jsx | 25 - .../src/examples/ParcelBoundaryPure.jsx | 39 +- .../src/examples/ParcelHocExample.jsx | 15 - .../examples/ParcelHocExampleDelayUntil.jsx | 47 -- .../ParcelHocExampleInitialValueFromProps.jsx | 19 - .../src/examples/ParcelHocExampleOnChange.jsx | 21 - .../src/examples/ParcelHocUpdateFromProps.jsx | 51 -- .../ParcelHocUpdateFromQueryString.jsx | 42 - .../ParcelMetaConfirmingDeletions.jsx | 31 +- .../src/examples/ParcelMetaSelections.jsx | 39 +- .../src/examples/SubmitButton.jsx | 51 +- .../src/examples/ValidationExample.jsx | 99 ++- .../src/pages/api/ParcelBoundary.jsx | 26 +- .../src/pages/api/ParcelBoundaryHoc.jsx | 18 +- .../src/pages/data-editing.md | 305 ++++---- .../examples/parcelboundary-forceUpdate.js | 12 - .../src/pages/examples/parcelboundary-hold.js | 12 - .../pages/examples/parcelhoc-delayuntil.js | 12 - .../pages/examples/parcelhoc-delayuntil.md | 55 -- .../src/pages/examples/parcelhoc-example.js | 12 - .../src/pages/examples/parcelhoc-example.md | 30 - .../src/pages/examples/parcelhoc-onchange.js | 12 - .../src/pages/examples/parcelhoc-onchange.md | 43 - .../examples/parcelhoc-updatefromprops.js | 14 - .../examples/parcelhoc-updatefromprops.md | 129 --- .../examples/parcelhoc-valuefromprops.js | 12 - .../examples/parcelhoc-valuefromprops.md | 37 - .../src/pages/getting-started.md | 54 +- .../dataparcels-docs/src/pages/sandbox.js | 9 +- .../dataparcels-docs/src/pages/sandbox.md | 8 - .../src/pages/ui-behaviour.md | 413 +++++----- .../src/sandbox/SandboxFrame.jsx | 31 - packages/dataparcels-docs/yalc.lock | 2 +- packages/dataparcels-docs/yarn.lock | 12 +- packages/dataparcels/README.md | 2 +- packages/dataparcels/package.json | 3 +- packages/dataparcels/src/index.js | 2 +- packages/dataparcels/src/types/Types.js | 4 +- .../dataparcels/src/validation/Validation.js | 16 +- .../validation/__test__/Validation-test.js | 48 +- .../react-dataparcels/ParcelBufferControl.js | 2 + packages/react-dataparcels/README.md | 2 +- .../__test__/Exports-test.js | 18 + packages/react-dataparcels/jest.config.js | 2 +- packages/react-dataparcels/package.json | 7 +- .../react-dataparcels/src/ParcelBoundary.jsx | 366 ++------- ...js => ParcelBoundaryControlDeprecated.jsx} | 0 .../src/ParcelBoundaryDeprecated.jsx | 342 ++++++++ .../src/ParcelBoundaryHoc.jsx | 9 +- .../src/ParcelBufferControl.js | 26 + packages/react-dataparcels/src/ParcelHoc.jsx | 9 +- .../src/__test__/ParcelBoundary-test.js | 737 +----------------- .../src/__test__/ParcelBoundaryHoc-test.js | 2 +- .../src/__test__/useParcelBuffer-test.js | 457 +++++++++++ .../useParcelBufferInternalKeepValue-test.js | 143 ++++ .../src/__test__/useParcelState-test.js | 182 +++++ .../react-dataparcels/src/useParcelBuffer.js | 162 ++++ .../src/useParcelBufferInternalBuffer.js | 64 ++ .../src/useParcelBufferInternalKeepValue.js | 48 ++ .../react-dataparcels/src/useParcelState.js | 66 ++ .../src/util/ApplyBeforeChange.js | 10 + .../src/util/ApplyModifyBeforeUpdate.js | 8 - .../__test__/pipeWithFakePrevParcel-test.js | 28 + .../src/util/pipeWithFakePrevParcel.js | 22 + packages/react-dataparcels/useParcelBuffer.js | 2 + packages/react-dataparcels/useParcelState.js | 2 + packages/react-dataparcels/yarn.lock | 12 +- 107 files changed, 2549 insertions(+), 2935 deletions(-) create mode 100644 packages/dataparcels-docs/src/component/exampleFrame.jsx rename packages/dataparcels-docs/src/docs/api/parcelBoundary/{modifyBeforeUpdate.md => beforeChange.md} (65%) delete mode 100644 packages/dataparcels-docs/src/docs/api/parcelBoundary/debugBuffer.md delete mode 100644 packages/dataparcels-docs/src/docs/api/parcelBoundary/debugParcel.md delete mode 100644 packages/dataparcels-docs/src/docs/api/parcelBoundary/onCancel.md delete mode 100644 packages/dataparcels-docs/src/docs/api/parcelBoundary/onRelease.md delete mode 100644 packages/dataparcels-docs/src/examples/EditingArraysFlipMove.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ModifyConditional.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelBoundaryForceUpdate.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelBoundaryHold.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocExample.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocExampleDelayUntil.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocExampleInitialValueFromProps.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocExampleOnChange.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocUpdateFromProps.jsx delete mode 100644 packages/dataparcels-docs/src/examples/ParcelHocUpdateFromQueryString.jsx delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelboundary-forceUpdate.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelboundary-hold.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.md delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-example.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-example.md delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.md delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.md delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.js delete mode 100644 packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.md delete mode 100644 packages/dataparcels-docs/src/pages/sandbox.md delete mode 100644 packages/dataparcels-docs/src/sandbox/SandboxFrame.jsx create mode 100644 packages/react-dataparcels/ParcelBufferControl.js rename packages/react-dataparcels/src/{ParcelBoundaryControl.js => ParcelBoundaryControlDeprecated.jsx} (100%) create mode 100644 packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx create mode 100644 packages/react-dataparcels/src/ParcelBufferControl.js create mode 100644 packages/react-dataparcels/src/__test__/useParcelBuffer-test.js create mode 100644 packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js create mode 100644 packages/react-dataparcels/src/__test__/useParcelState-test.js create mode 100644 packages/react-dataparcels/src/useParcelBuffer.js create mode 100644 packages/react-dataparcels/src/useParcelBufferInternalBuffer.js create mode 100644 packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js create mode 100644 packages/react-dataparcels/src/useParcelState.js create mode 100644 packages/react-dataparcels/src/util/ApplyBeforeChange.js delete mode 100644 packages/react-dataparcels/src/util/ApplyModifyBeforeUpdate.js create mode 100644 packages/react-dataparcels/src/util/__test__/pipeWithFakePrevParcel-test.js create mode 100644 packages/react-dataparcels/src/util/pipeWithFakePrevParcel.js create mode 100644 packages/react-dataparcels/useParcelBuffer.js create mode 100644 packages/react-dataparcels/useParcelState.js diff --git a/README.md b/README.md index c75cf74b..f24dd849 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ A library for editing data structures that works really well with React. ## Packages -If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get components and hocs to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). +If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get hooks, hocs and components to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). diff --git a/packages/dataparcels-docs/package.json b/packages/dataparcels-docs/package.json index ae6b7537..302e6462 100644 --- a/packages/dataparcels-docs/package.json +++ b/packages/dataparcels-docs/package.json @@ -33,7 +33,6 @@ "react-dataparcels": "file:.yalc/react-dataparcels", "react-dataparcels-drag": "file:.yalc/react-dataparcels-drag", "react-dom": "^16.8.6", - "react-flip-move": "^3.0.2", "react-helmet": "^5.2.0", "react-sortable-hoc": "^1.4.0", "react-spruce": "^0.1.1", diff --git a/packages/dataparcels-docs/src/component/APINavigation.jsx b/packages/dataparcels-docs/src/component/APINavigation.jsx index a3c83847..f3f6b287 100644 --- a/packages/dataparcels-docs/src/component/APINavigation.jsx +++ b/packages/dataparcels-docs/src/component/APINavigation.jsx @@ -1,16 +1,18 @@ // @flow import React from "react"; -import {NavigationList, NavigationListItem} from 'dcme-style'; +import {NavigationList, NavigationListItem, Text} from 'dcme-style'; import Link from 'component/Link'; export default () => Api - Parcel - - ParcelHoc + - useParcelState + - useParcelBuffer - ParcelBoundary - - ParcelBoundaryHoc - ParcelShape - ChangeRequest - CancelActionMarker - shape + - ParcelHoc + - ParcelBoundaryHoc ; diff --git a/packages/dataparcels-docs/src/component/exampleFrame.jsx b/packages/dataparcels-docs/src/component/exampleFrame.jsx new file mode 100644 index 00000000..72e5d8f3 --- /dev/null +++ b/packages/dataparcels-docs/src/component/exampleFrame.jsx @@ -0,0 +1,70 @@ +// @flow + +import type {ComponentType} from 'react'; +import type {LayoutElement} from 'dcme-style'; +import type {Node} from 'react'; + +import filter from 'unmutable/lib/filter'; +import map from 'unmutable/lib/map'; +import toArray from 'unmutable/lib/toArray'; +import pipeWith from 'unmutable/lib/util/pipeWith'; + +import React from 'react'; +import {Box, Grid, GridItem, Layout, Terminal, Text} from 'dcme-style'; + +type Props = {}; + +type LayoutProps = { + demo: LayoutElement, + data: LayoutElement, +}; + +const stringify = (value) => { + let replacer = (key, value) => { + // The following works because NaN is the only value in javascript which is not equal to itself. + if(value !== value) { + return 'NaN'; + } + return value; + }; + return `${JSON.stringify(value, replacer, 4)}`.replace(/"NaN"/g, "NaN"); +}; + +class ExampleFrame extends Layout { + static elements = ['demo', 'data']; + + static layout = ({demo, data}) => + + + + {demo()} + + + {data()} + + + + ; + + demo = (): Node => { + return this.props.children; + }; + + data = (): Node => { + return pipeWith( + this.props.parcels, + map((parcel, key) => + {key} + {stringify(parcel.value)} + ), + toArray() + ); + }; +}; + +export default (parcels: Object, children) => { + return ; +}; diff --git a/packages/dataparcels-docs/src/content/API.js b/packages/dataparcels-docs/src/content/API.js index 11a37b8d..b702dcb6 100644 --- a/packages/dataparcels-docs/src/content/API.js +++ b/packages/dataparcels-docs/src/content/API.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import Link from 'gatsby-link'; -import {Box, Grid, GridItem, Image, NavigationList, NavigationListItem, Text} from 'dcme-style'; +import {Box, BulletList, BulletListItem, Grid, GridItem, Image, NavigationList, NavigationListItem, Text} from 'dcme-style'; import SpruceClassName from 'react-spruce/lib/SpruceClassName'; import IconParcel from 'content/parcel.gif'; @@ -36,28 +36,36 @@ export default () => image={IconParcel} /> - ParcelHoc is a React higher order component. - Its job is to provide a parcel as a prop, and to handle how the parcel binds to React props and lifecycle events. + useParcelState is a React hook. + Its job is to provide a parcel stored in state, and to handle how the parcel binds to React props. } image={IconParcelHoc} /> - ParcelBoundary is a React component. - Its job is to optimise rendering performance, and to optionally control the flow of parcel changes. + useParcelBuffer is a React hook. + Its job is to control the flow of parcel changes. } - image={IconParcelBoundary} + image={IconParcelBoundaryHoc} /> - ParcelBoundaryHoc is a React higher order component. - Its job is to control the flow of parcel changes. It is the higher order component version of a ParcelBoundary. + ParcelBoundary is a React component. + Its job is to optimise rendering performance, and to optionally control the flow of parcel changes using useParcelBuffer. } - image={IconParcelBoundaryHoc} + image={IconParcelBoundary} /> - See also: ParcelShape, ChangeRequest, CancelActionMarker, shape. + See also: + + ChangeRequest + CancelActionMarker + ParcelShape + shape + ParcelHoc + ParcelBoundaryHoc + ; diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/ParcelBoundary.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/ParcelBoundary.md index 3b3ea2d3..d6dad683 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/ParcelBoundary.md +++ b/packages/dataparcels-docs/src/docs/api/parcelBoundary/ParcelBoundary.md @@ -1,9 +1,6 @@ import Link from 'component/Link'; import Param from 'component/Param'; import ApiPageIcon from 'component/ApiPageIcon'; -import ParcelHocExample from 'pages/examples/parcelhoc-example.md'; -import ParcelHocInitialValueFromProps from 'pages/examples/parcelhoc-valuefromprops.md'; -import ParcelHocOnChange from 'pages/examples/parcelhoc-onchange.md'; import IconParcelBoundary from 'content/parcelboundary.gif'; import {Box, Message} from 'dcme-style'; @@ -15,7 +12,7 @@ ParcelBoundary is a React component. Its job is to optimise rendering performanc Each ParcelBoundary is passed a Parcel. By default the ParcelBoundary uses pure rendering, and will only update when the Parcel's data changes to avoid unnecessary re-rendering. -ParcelBoundaries have an internal action buffer that can hold onto changes as they exit the boundary. These are normally released immediately, but also allow for debouncing changes, or putting a hold on all changes so they can be released later. +ParcelBoundaries have an internal buffer that can hold onto changes as they exit the boundary. These are normally released immediately, but also allow for debouncing changes, or putting a hold on all changes so they can be released later. Internally ParcelBoundaries use a [useParcelBuffer](/api/usePaercelBuffer) hook. ```js import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; @@ -24,20 +21,16 @@ import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; ```js } + forceUpdate={?any[]} + debounce={?number} hold={?boolean} - modifyBeforeUpdate={?Array} - onCancel={?Array} - onRelease={?Array} - debugBuffer={?boolean} - debugParcel={?boolean} + beforeChange={?Function|Function[]} > {(parcel, control) => Node} ``` - ParcelBoundary is also available as a React higher order component, ParcelBoundaryHoc. + ParcelBoundary is also available as a hook, useParcelHook. diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/modifyBeforeUpdate.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/beforeChange.md similarity index 65% rename from packages/dataparcels-docs/src/docs/api/parcelBoundary/modifyBeforeUpdate.md rename to packages/dataparcels-docs/src/docs/api/parcelBoundary/beforeChange.md index 297cafd9..d7022c01 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/modifyBeforeUpdate.md +++ b/packages/dataparcels-docs/src/docs/api/parcelBoundary/beforeChange.md @@ -3,7 +3,7 @@ import Link from 'component/Link'; import ValueUpdater from 'docs/notes/ValueUpdater.md'; ```flow -modifyBeforeUpdate?: Array +beforeChange?: Updater|Updater[] // updating value - only to be used if shape doesn't change type Updater = (value: any, changeRequest: ChangeRequest) => any; @@ -12,14 +12,14 @@ type Updater = (value: any, changeRequest: ChangeRequest) => any; type Updater = shape((parcelShape: ParcelShape, changeRequest: ChangeRequest) => any); ``` -The `modifyBeforeUpdate` prop allows derived data to be set on the Parcel in the ParcelBoundary. -Whenever the data in a ParcelBoundary is about to be initialised or updated in any way, it is passed through all `modifyBeforeUpdate` functions. +The `beforeChange` prop allows derived data to be set on the Parcel in the ParcelBoundary. +Whenever the data in a ParcelBoundary is about to be initialised or updated in any way, it is passed through all `beforeChange` functions. -Each function in the `modifyBeforeUpdate` array operates just like the `updater` provided to Parcel.modifyUp(). +Each function in the `beforeChange` array operates just like the `updater` provided to Parcel.modifyUp(). ```js -let modifyBeforeUpdate = [ +let beforeChange = [ (value) => ({ ...value, isLong: value.word.length > 15 @@ -33,7 +33,7 @@ let modifyBeforeUpdate = [ // // ^ this will contain derived data // } - + {(exampleParcel) => } diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/childRenderer.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/childRenderer.md index c043e19e..ee94b96f 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/childRenderer.md +++ b/packages/dataparcels-docs/src/docs/api/parcelBoundary/childRenderer.md @@ -1,36 +1,35 @@ import Link from 'component/Link'; ```flow -(parcel: Parcel, control: ParcelBoundaryControl) => Node +(parcel: Parcel, buffer: ParcelBufferControl) => Node -type ParcelBoundaryControl = { - release: () => void, - cancel: () => void, +type ParcelBufferControl = { + submit: () => void, + reset: () => void, buffered: boolean, - buffer: Action[] + actions: Action[] } ``` ParcelBoundaries must be given a `childRenderer` function as children. This is called whenever the ParcelBoundary updates. -It is passed a `parcel` and a ParcelBoundaryControl instance. +It is passed a `parcel` and a ParcelBufferControl instance. - The `parcel` is on the "inside" of the parcel boundary, and is able to update independently of the parcel that was passed into the ParcelBoundary. -- The `control` argument passes a ParcelBoundaryControl which can be used to control the ParcelBoundary's action buffer (see the hold example) and information about the current state of the action buffer. +- The `buffer` argument passes a ParcelBufferControl which can be used to control the ParcelBoundary's action buffer and information about the current state of the action buffer. -#### ParcelBoundaryControl +#### ParcelBufferControl -- The `release()` function will release any changes in the buffer, allowing them to propagate upward out of the ParcelBoundary. -- The `cancel()` function will cancel any changes in the buffer. -- The `buffered` boolean indicates if the ParcelBoundary currently contains changes that it hasn't yet released. -- The `buffer` array contains the actions that are currently held in the buffer. -- `originalParcel` contains the Parcel that was passed into the ParcelBoundary, unaffected by any buffering or ParcelBoundary state. +- The `submit()` function will submit changes in the buffer, allowing them to propagate upward out of the ParcelBoundary. +- The `reset()` function will reset the buffer, clearing any changes in the buffer. +- The `buffered` boolean indicates if the ParcelBoundary currently contains changes that it hasn't yet submitted. +- The `actions` array contains the actions that are currently held in the buffer. The return value of `childRenderer` will be rendered. ```js // personParcel is a Parcel - {(parcel, control) => { + {(parcel, buffer) => { return ; }} diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugBuffer.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugBuffer.md deleted file mode 100644 index 44aa6bf3..00000000 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugBuffer.md +++ /dev/null @@ -1,5 +0,0 @@ -```flow -debugBuffer?: boolean = false // optional -``` - -Wehn `debugBuffer` is true, ParcelBoundary will log out changes relating to its internal action buffer. diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugParcel.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugParcel.md deleted file mode 100644 index 7e24a640..00000000 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/debugParcel.md +++ /dev/null @@ -1,5 +0,0 @@ -```flow -debugParcel?: boolean = false // optional -``` - -Wehn `debugParcel` is true, ParcelBoundary will log out changes to its current parcel state. diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/forceUpdate.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/forceUpdate.md index 7f93572e..714b58d3 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/forceUpdate.md +++ b/packages/dataparcels-docs/src/docs/api/parcelBoundary/forceUpdate.md @@ -1,7 +1,7 @@ import Link from 'component/Link'; ```flow -forceUpdate?: Array<*> // optional +forceUpdate?: any[] // optional ``` While a ParcelBoundary is using pure rendering, `forceUpdate` will force the ParcelBoundary to re-render in response to changes in other props. Each item in the `forceUpdate` array is compared using strict equality against its previous values, and if any are not strictly equal, the ParcelBoundary will re-render. @@ -14,5 +14,3 @@ While a ParcelBoundary is using pure rendering, `forceUpdate` will force the Par {(personParcel) => } ; -// without keepValue, if you type "0.10" in the input above it would -// immediately be replaced with "0.1", as the new value is turned -// into a number on the way up, and into a string on the way down, -// which would make typing very frustrating. - -// keepValue keeps "0.10" in the text field. ``` -Example +The `keepValue` prop is necessary here to allow the ParcelBoundary to be the master of its own state, at least in regards to changes that come from itself. So even when a non-number is entered into the input (e.g. "A"), and this is turned into `NaN` as it passes through `.modifyUp()`, the ParcelBoundary can still remember that it should contain "A". + +Other values that are preserved in this example are "0.10", which would be turned into "0.1" by the modify functions, and "0.0000001", which would be turned into "1e-7". + +**[See an example of keepValue](/data-editing#Modifying-data-to-fit-the-UI)** diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/onCancel.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/onCancel.md deleted file mode 100644 index 4edd2853..00000000 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/onCancel.md +++ /dev/null @@ -1,18 +0,0 @@ -import Link from 'component/Link'; - -```flow -onCancel?: Array // optional - -type ContinueChainFunction = (continueCancel: Function) => void -``` - -The `onCancel` function array can be used to add behaviour before or after a ParcelBoundary cancels the changes in its buffer. - -If `onCancel` is provided and `ParcelBoundaryControl.cancel()` is called, the buffer's changes are *not* immediately cancelled, and the first element of `onCancel` is called instead. The `onCancel` function is passed a `continueCancel()` function as an argument, and if this `continueCancel()` function is called then it will call the next element of the `onCancel` array. If `continueCancel()` is called on the last element in the `onCancel` array, then it will cancel the changes in the buffer. - -```js -// add a delay to the cancel action -let onCancel = continueCancel => setTimeout(continueCancel, 1000); - -// ... -``` diff --git a/packages/dataparcels-docs/src/docs/api/parcelBoundary/onRelease.md b/packages/dataparcels-docs/src/docs/api/parcelBoundary/onRelease.md deleted file mode 100644 index 9235846d..00000000 --- a/packages/dataparcels-docs/src/docs/api/parcelBoundary/onRelease.md +++ /dev/null @@ -1,18 +0,0 @@ -import Link from 'component/Link'; - -```flow -onRelease?: Array // optional - -type ContinueChainFunction = (continueRelease: Function) => void -``` - -The `onRelease` function array can be used to add behaviour before or after a ParcelBoundary releases the changes in its buffer. - -If `onRelease` is provided and `ParcelBoundaryControl.release()` is called, the buffer's changes are *not* immediately released, and the first element of `onRelease` is called instead. The `onRelease` function is passed a `continueRelease()` function as an argument, and if this `continueRelease()` function is called then it will call the next element of the `onRelease` array. If `continueRelease()` is called on the last element in the `onRelease` array, then it will release the changes in the buffer. - -```js -// add a delay to the release action -let onRelease = continueRelease => setTimeout(continueRelease, 1000); - -// ... -``` diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/ParcelHoc.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/ParcelHoc.md index 5cdf6639..79af54e9 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/ParcelHoc.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/ParcelHoc.md @@ -1,9 +1,6 @@ import Link from 'component/Link'; import Param from 'component/Param'; import ApiPageIcon from 'component/ApiPageIcon'; -import ParcelHocExample from 'pages/examples/parcelhoc-example.md'; -import ParcelHocInitialValueFromProps from 'pages/examples/parcelhoc-valuefromprops.md'; -import ParcelHocOnChange from 'pages/examples/parcelhoc-onchange.md'; import IconParcelHoc from 'content/parcelhoc.gif'; # ParcelHoc diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/delayUntil.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/delayUntil.md index c2886a9e..8fb404cc 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/delayUntil.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/delayUntil.md @@ -5,5 +5,3 @@ delayUntil?: (props: Object) => boolean // optional ``` You can delay the creation of the parcel by providing an `delayUntil` function. It will be called on mount and at every prop change until the parcel is created. It is passed `props`, and the Parcel will not be created until `true` is returned. A value of `undefined` will be passed down until the Parcel is created. Once the returned value is `true`, the Parcel will be created with the props at that time. - -Example diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/name.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/name.md index 9c1baf92..6f23ed31 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/name.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/name.md @@ -5,5 +5,3 @@ name: string ``` Sets the name of the prop that will contain the parcel. - -Example diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/onChange.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/onChange.md index 5f355d80..3f1c0c94 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/onChange.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/onChange.md @@ -7,5 +7,3 @@ onChange?: (props: Object) => (value: any, changeRequest: ChangeRequest) => void The `onChange` function is called whenever ParcelHoc changes. It expects to be given a double barrel function. The first function will be passed `props`, and the next is passed the new value. It is only fired if the value actually changes. `onChange` is often used to relay changes further up the React DOM heirarchy. This works in a very similar way to [uncontrolled components in React](https://reactjs.org/docs/uncontrolled-components.html). - -Example diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/shouldParcelUpdateFromProps.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/shouldParcelUpdateFromProps.md index 2d95ac8a..c805c91c 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/shouldParcelUpdateFromProps.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/shouldParcelUpdateFromProps.md @@ -25,5 +25,3 @@ ParcelHoc({ In future there will be more options to allow key and meta data to be retained. - -Example diff --git a/packages/dataparcels-docs/src/docs/api/parcelHoc/valueFromProps.md b/packages/dataparcels-docs/src/docs/api/parcelHoc/valueFromProps.md index aaed186f..a83ada3f 100644 --- a/packages/dataparcels-docs/src/docs/api/parcelHoc/valueFromProps.md +++ b/packages/dataparcels-docs/src/docs/api/parcelHoc/valueFromProps.md @@ -5,5 +5,3 @@ valueFromProps: (props: Object) => any ``` The `valueFromProps` function will be called once when ParcelHoc mounts. It is passed `props`, and the returned value is used as the initial value of the ParcelHoc's Parcel. - -Example diff --git a/packages/dataparcels-docs/src/docs/api/validation/Validation.md b/packages/dataparcels-docs/src/docs/api/validation/Validation.md index d064277f..26215ecc 100644 --- a/packages/dataparcels-docs/src/docs/api/validation/Validation.md +++ b/packages/dataparcels-docs/src/docs/api/validation/Validation.md @@ -28,15 +28,16 @@ const validation = Validation({ // example validator const validateStringNotBlank = (name) => (value) => { - return (!value || value.trim().length === 0) && `${name} must not be blank`; + return value && value.trim().length > 0 + ? null // return null if the data is valid + : `${name} must not be blank`; }; // usage -ParcelBoundary({ - name: 'exampleParcel', +useParcelBuffer({ hold: true, - modifyBeforeUpdate: [validation.modifyBeforeUpdate] + beforeChange: validation.beforeChange // ^ run validator before data updates // to set meta on Parcels that have failed validation }); diff --git a/packages/dataparcels-docs/src/docs/api/validation/ValidationResult.md b/packages/dataparcels-docs/src/docs/api/validation/ValidationResult.md index b2e9f7f8..ccdf4e89 100644 --- a/packages/dataparcels-docs/src/docs/api/validation/ValidationResult.md +++ b/packages/dataparcels-docs/src/docs/api/validation/ValidationResult.md @@ -1,15 +1,14 @@ ```flow ValidationResult = { - modifyBeforeUpdate: ParcelValueUpdater, - onRelease: ContinueChainFunction + beforeChange: ParcelValueUpdater }; ``` -The Validation function returns an object containing two functions. +The Validation function returns an object containing a single function. -#### modifyBeforeUpdate +#### beforeChange -This is a function that can be used directly with `modifyBeforeUpdate` on [ParcelHoc](/api/ParcelHoc#modifyBeforeUpdate"), [ParcelBoundary](/api/ParcelBoundary#modifyBeforeUpdate) or [ParcelBoundaryHoc](/api/ParcelBoundaryHoc#modifyBeforeUpdate). +This is a function that can be used directly with `beforeChange` on [useParcelState](/api/useParcelState#beforeChange"), [useParcelBuffer](/api/useParcelBuffer#beforeChange) or [ParcelBoundary](/api/ParcelBoundary#beforeChange). This will check the Parcel's value and set [Parcel meta](/parcel-meta) wherever validations errors occured. @@ -21,23 +20,8 @@ Please refer to the UI Behaviour page to see [a full example](/ui-behaviour#Vali ParcelBoundary({ name: 'exampleParcel', hold: true, - modifyBeforeUpdate: [validation.modifyBeforeUpdate] + beforeChange: validation.beforeChange // ^ run validator before data updates // to set meta on Parcels that have failed validation }); ``` - -#### onRelease - -This is a function that can be used directly with `onRelease` on [ParcelBoundary](/api/ParcelBoundary#modifyBeforeUpdate) or [ParcelBoundaryHoc](/api/ParcelBoundaryHoc#modifyBeforeUpdate). - -If `release()` is called, this `onRelease` function will prevent the release from taking place if any data is invalid. - -```js -ParcelBoundary({ - name: 'exampleParcel', - hold: true, - modifyBeforeUpdate: [validation.modifyBeforeUpdate] - onRelease: [validation.onRelease] -}); -``` diff --git a/packages/dataparcels-docs/src/examples/Autosave.jsx b/packages/dataparcels-docs/src/examples/Autosave.jsx index 15e4ef45..8e1234a6 100644 --- a/packages/dataparcels-docs/src/examples/Autosave.jsx +++ b/packages/dataparcels-docs/src/examples/Autosave.jsx @@ -1,13 +1,24 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; -import ExampleHoc from 'component/ExampleHoc'; -import composeWith from 'unmutable/composeWith'; +import exampleFrame from 'component/exampleFrame'; -const PersonEditor = (props) => { - let {personParcel, personParcelControl} = props; - return
+export default function PersonEditor(props) { + + let [personParcelState] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps" + } + }); + + let [personParcel] = useParcelBuffer({ + parcel: personParcelState, + debounce: 500 // hold onto changes until 500ms have elapsed since last change + }); + + return exampleFrame({personParcelState, personParcel},
{(firstname) => } @@ -17,23 +28,6 @@ const PersonEditor = (props) => { {(lastname) => } -
; -}; - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) +
); +} -export default composeWith( - ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps" - }) - }), - ExampleHoc, - ParcelBoundaryHoc({ - name: "personParcel", - debounce: 500 // hold onto changes until 500ms have elapsed since last change - }), - PersonEditor -); diff --git a/packages/dataparcels-docs/src/examples/DerivedMeta.jsx b/packages/dataparcels-docs/src/examples/DerivedMeta.jsx index 262f73e2..f0cc7e9d 100644 --- a/packages/dataparcels-docs/src/examples/DerivedMeta.jsx +++ b/packages/dataparcels-docs/src/examples/DerivedMeta.jsx @@ -1,36 +1,28 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import shape from 'react-dataparcels/shape'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -// this example uses a shape updater to set meta data -const setWordLengthMeta = shape(parcelShape => { - let word = parcelShape.value; - return parcelShape.setMeta({ - wordLength: word.length - }); -}); +const setWordLengthMeta = shape(parcelShape => parcelShape.setMeta({ + wordLength: parcelShape.value.word.length +})); + +export default function WordEditor(props) { -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (/* props */) => "blueberries", - modifyBeforeUpdate: [ - setWordLengthMeta - ] -}); + let [wordParcel] = useParcelState({ + value: { + word: "blueberries", + wordLength: undefined + }, + beforeChange: setWordLengthMeta + }); -const WordEditor = (props) => { - let {wordParcel} = props; - return
+ return exampleFrame({wordParcel},
- - {(parcel) =>
- -

length is {parcel.meta.wordLength}

-
} + + {(parcel) => } -
; -}; - -export default WordParcelHoc(ExampleHoc(WordEditor)); +

word length is {wordParcel.meta.wordLength}

+
); +} diff --git a/packages/dataparcels-docs/src/examples/DerivedValue.jsx b/packages/dataparcels-docs/src/examples/DerivedValue.jsx index 8827b613..474e299e 100644 --- a/packages/dataparcels-docs/src/examples/DerivedValue.jsx +++ b/packages/dataparcels-docs/src/examples/DerivedValue.jsx @@ -1,31 +1,26 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (/* props */) => ({ - word: "blueberries", - uppercase: undefined - }), - modifyBeforeUpdate: [ - (value) => ({ +export default function WordEditor(props) { + + let [wordParcel] = useParcelState({ + value: { + word: "blueberries", + wordLength: undefined + }, + beforeChange: (value) => ({ word: value.word, - uppercase: value.word.toUpperCase() + wordLength: value.word.length }) - ] -}); + }); -const WordEditor = (props) => { - let {wordParcel} = props; - return
+ return exampleFrame({wordParcel},
{(parcel) => } -

Uppercase word is {wordParcel.get('uppercase').value}

-
; -}; - -export default WordParcelHoc(ExampleHoc(WordEditor)); +

word length is {wordParcel.get('wordLength').value}

+
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingArrays.jsx b/packages/dataparcels-docs/src/examples/EditingArrays.jsx index 4c576725..f7e88946 100644 --- a/packages/dataparcels-docs/src/examples/EditingArrays.jsx +++ b/packages/dataparcels-docs/src/examples/EditingArrays.jsx @@ -1,20 +1,19 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +export default function FruitListEditor(props) { -const FruitListEditor = (props) => { - let {fruitListParcel} = props; - return
+ let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + + return exampleFrame({fruitListParcel},
{fruitListParcel.toArray((fruitParcel) => { return {(parcel) =>
@@ -28,7 +27,5 @@ const FruitListEditor = (props) => { ; })} -
; -}; - -export default FruitListParcelHoc(ExampleHoc(FruitListEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingArraysDrag.jsx b/packages/dataparcels-docs/src/examples/EditingArraysDrag.jsx index 9bb43410..09e828c6 100644 --- a/packages/dataparcels-docs/src/examples/EditingArraysDrag.jsx +++ b/packages/dataparcels-docs/src/examples/EditingArraysDrag.jsx @@ -1,17 +1,8 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import ParcelDrag from 'react-dataparcels-drag'; -import ExampleHoc from 'component/ExampleHoc'; - -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +import exampleFrame from 'component/exampleFrame'; const SortableFruitList = ParcelDrag({ element: (fruitParcel) => @@ -23,12 +14,18 @@ const SortableFruitList = ParcelDrag({ }); -const FruitListEditor = (props) => { - let {fruitListParcel} = props; - return
+export default function FruitListEditor(props) { + + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + + return exampleFrame({fruitListParcel},
-
; -}; - -export default FruitListParcelHoc(ExampleHoc(FruitListEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingArraysFlipMove.jsx b/packages/dataparcels-docs/src/examples/EditingArraysFlipMove.jsx deleted file mode 100644 index 27fe89fc..00000000 --- a/packages/dataparcels-docs/src/examples/EditingArraysFlipMove.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import FlipMove from 'react-flip-move'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); - -const FruitListEditor = (props) => { - let {fruitListParcel} = props; - return - {fruitListParcel.toArray((fruitParcel) => { - return - {(parcel) =>
- - - - - -
} -
; - })} - -
; -}; - -export default FruitListParcelHoc(ExampleHoc(FruitListEditor)); diff --git a/packages/dataparcels-docs/src/examples/EditingModifyAlphanumeric.jsx b/packages/dataparcels-docs/src/examples/EditingModifyAlphanumeric.jsx index 3bd2e272..de16f8d2 100644 --- a/packages/dataparcels-docs/src/examples/EditingModifyAlphanumeric.jsx +++ b/packages/dataparcels-docs/src/examples/EditingModifyAlphanumeric.jsx @@ -1,14 +1,9 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const AlphanumericParcelHoc = ParcelHoc({ - name: "alphanumericParcel", - valueFromProps: (/* props */) => "Abc123" -}); - -const AlphanumericInput = (props) => { +function AlphanumericInput(props) { return {(alphanumericParcel) => { let parcel = alphanumericParcel.modifyUp(string => string.replace(/[^a-zA-Z0-9]/g, "")); @@ -16,15 +11,17 @@ const AlphanumericInput = (props) => { return ; }} ; -}; +} + +export default function AlphanumericEditor(props) { -const AlphanumericEditor = (props) => { - let {alphanumericParcel} = props; - return
+ let [alphanumericParcel] = useParcelState({ + value: "Abc123" + }); + + return exampleFrame({alphanumericParcel},

Alphanumeric input

Disallows all non-alphanumeric characters. Try typing some punctuation.

-
; -}; - -export default AlphanumericParcelHoc(ExampleHoc(AlphanumericEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingModifyDelimited.jsx b/packages/dataparcels-docs/src/examples/EditingModifyDelimited.jsx index b8ee0eaf..39d2a04b 100644 --- a/packages/dataparcels-docs/src/examples/EditingModifyDelimited.jsx +++ b/packages/dataparcels-docs/src/examples/EditingModifyDelimited.jsx @@ -1,14 +1,9 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const DelimitedStringParcelHoc = ParcelHoc({ - name: "delimitedParcel", - valueFromProps: (/* props */) => "abc.def" -}); - -const DelimitedStringInput = (props) => { +function DelimitedStringInput(props) { let delimitedStringParcel = props .delimitedStringParcel .modifyDown(string => string.split(".")) @@ -29,15 +24,17 @@ const DelimitedStringInput = (props) => { })}
; -}; +} -const DelimitedStringEditor = (props) => { - let {delimitedParcel} = props; - return
+export default function DelimitedStringEditor(props) { + + let [delimitedParcel] = useParcelState({ + value: "abc.def" + }); + + return exampleFrame({delimitedParcel},

Delimited string > array of strings

Turns a stored string into an array so array editing controls can be rendered.

-
; -}; - -export default DelimitedStringParcelHoc(ExampleHoc(DelimitedStringEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingModifyMissing.jsx b/packages/dataparcels-docs/src/examples/EditingModifyMissing.jsx index 397f9200..4ce216c5 100644 --- a/packages/dataparcels-docs/src/examples/EditingModifyMissing.jsx +++ b/packages/dataparcels-docs/src/examples/EditingModifyMissing.jsx @@ -1,19 +1,14 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const ExampleParcelHoc = ParcelHoc({ - name: "maybeArrayParcel", - valueFromProps: (/* props */) => undefined -}); - -const MaybeArrayInput = (props) => { +function MaybeArrayInput(props) { let maybeArrayParcel = props .maybeArrayParcel .modifyDown(value => value || []) // ^ turn value into an array if its missing - // so we can always render against an array + // so we can always render as though an array exists return
{maybeArrayParcel.toArray((segmentParcel) => { @@ -23,15 +18,17 @@ const MaybeArrayInput = (props) => { })}
; -}; +} -const MaybeArrayEditor = (props) => { - let {maybeArrayParcel} = props; - return
+export default function MaybeArrayEditor(props) { + + let [maybeArrayParcel] = useParcelState({ + value: undefined + }); + + return exampleFrame({maybeArrayParcel},

Compensating for missing values

Prepares values so that editors can remain simple.

-
; -}; - -export default ExampleParcelHoc(ExampleHoc(MaybeArrayEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingModifyNumber.jsx b/packages/dataparcels-docs/src/examples/EditingModifyNumber.jsx index 22a58731..789ba918 100644 --- a/packages/dataparcels-docs/src/examples/EditingModifyNumber.jsx +++ b/packages/dataparcels-docs/src/examples/EditingModifyNumber.jsx @@ -1,14 +1,9 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const NumberParcelHoc = ParcelHoc({ - name: "numberParcel", - valueFromProps: (/* props */) => 123 -}); - -const NumberInput = (props) => { +function NumberInput(props) { let numberParcel = props .numberParcel .modifyUp(string => Number(string)) @@ -17,24 +12,22 @@ const NumberInput = (props) => { // ^ turn value into a string on the way down // and turn value back into a number on the way up - // without the keepValue prop, typing "0.10" - // would immediately be replaced with "0.1" - // as the new value is turned into a number on the way up, - // and into a string on the way down - // which would make typing very frustrating + // *the keepValue prop is necessary here, see note below return {(parcel) => } ; -}; +} + +export default function NumberEditor(props) { -const NumberEditor = (props) => { - let {numberParcel} = props; - return
+ let [numberParcel] = useParcelState({ + value: 123 + }); + + return exampleFrame({numberParcel},

Number > string

Turns a stored number into a string for editing.

-
; -}; - -export default NumberParcelHoc(ExampleHoc(NumberEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/EditingObjects.jsx b/packages/dataparcels-docs/src/examples/EditingObjects.jsx index 201e168c..44226610 100644 --- a/packages/dataparcels-docs/src/examples/EditingObjects.jsx +++ b/packages/dataparcels-docs/src/examples/EditingObjects.jsx @@ -1,37 +1,37 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps", - address: { - postcode: "1234" - } - }) -}); +export default function PersonEditor(props) { -const PersonEditor = (props) => { - let {personParcel} = props; - return
- - - {(firstname) => } - + let [personParcel] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps", + address: { + postcode: "1234" + } + } + }); - - - {(lastname) => } - + return exampleFrame( + {personParcel}, +
+ + + {(firstname) => } + - - - {(postcode) => } - -
; -}; + + + {(lastname) => } + -export default PersonParcelHoc(ExampleHoc(PersonEditor)); + + + {(postcode) => } + +
+ ); +} diff --git a/packages/dataparcels-docs/src/examples/EditingObjectsBeginner.jsx b/packages/dataparcels-docs/src/examples/EditingObjectsBeginner.jsx index c331c698..1f30b0e7 100644 --- a/packages/dataparcels-docs/src/examples/EditingObjectsBeginner.jsx +++ b/packages/dataparcels-docs/src/examples/EditingObjectsBeginner.jsx @@ -1,35 +1,34 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import exampleFrame from 'component/exampleFrame'; -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps", - address: { - postcode: "1234" - } - }) -}); +export default function PersonEditor(props) { -const PersonEditor = (props) => { - let {personParcel} = props; + let [personParcel] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps", + address: { + postcode: "1234" + } + } + }); let firstname = personParcel.get('firstname'); let lastname = personParcel.get('lastname'); let postcode = personParcel.getIn(['address', 'postcode']); - return
- - - - - + return exampleFrame( + {personParcel}, +
+ + - - -
; -}; + + -export default PersonParcelHoc(ExampleHoc(PersonEditor)); + + +
+ ); +} diff --git a/packages/dataparcels-docs/src/examples/InteractingFields.jsx b/packages/dataparcels-docs/src/examples/InteractingFields.jsx index 06d463e5..71a504b4 100644 --- a/packages/dataparcels-docs/src/examples/InteractingFields.jsx +++ b/packages/dataparcels-docs/src/examples/InteractingFields.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; import CancelActionMarker from 'react-dataparcels/CancelActionMarker'; +import exampleFrame from 'component/exampleFrame'; -const calculate = (value, changeRequest) => { +function calculate(value, changeRequest) { let {a, b, sum} = value; if(changeRequest.originPath[0] !== "sum") { @@ -27,19 +27,7 @@ const calculate = (value, changeRequest) => { } return {a, b, sum}; -}; - -const SumParcelHoc = ParcelHoc({ - name: "sumParcel", - valueFromProps: (/* props */) => ({ - a: 5, - b: 5, - sum: undefined - }), - modifyBeforeUpdate: [ - calculate - ] -}); +} // turn numbers into strings on the way down // and back into numbers on the way up @@ -55,9 +43,18 @@ const numberToString = (parcel) => parcel return (string === "" || isNaN(number)) ? CancelActionMarker : number; }); -const AreaEditor = (props) => { - let {sumParcel} = props; - return
+export default function AreaEditor(props) { + + let [sumParcel] = useParcelState({ + value: { + a: 5, + b: 5, + sum: undefined + }, + beforeChange: calculate + }); + + return exampleFrame({sumParcel},
{(parcel) => } @@ -72,7 +69,5 @@ const AreaEditor = (props) => { {(parcel) => } -
; -}; - -export default SumParcelHoc(ExampleHoc(AreaEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/ManagingOwnParcelState.jsx b/packages/dataparcels-docs/src/examples/ManagingOwnParcelState.jsx index 225c4d23..e041ac5b 100644 --- a/packages/dataparcels-docs/src/examples/ManagingOwnParcelState.jsx +++ b/packages/dataparcels-docs/src/examples/ManagingOwnParcelState.jsx @@ -1,34 +1,29 @@ import React from 'react'; +import {useState} from 'react'; import Parcel from 'react-dataparcels'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -export default class ManagingOwnParcelState extends React.Component { - constructor(props) { - super(props); +export default function ManagingOwnParcelState(props) { - let personParcel = new Parcel({ - value: { - firstname: "Robert", - lastname: "Clamps" - }, - handleChange: (personParcel) => this.setState({personParcel}) - }); + let [personParcel, setPersonParcel] = useState(() => new Parcel({ + value: { + firstname: "Robert", + lastname: "Clamps" + }, + handleChange: (parcel) => { + setPersonParcel(parcel); + } + })); - this.state = {personParcel}; - } + return
+ + + {(firstname) => } + - render() { - let {personParcel} = this.state; - return
- - - {(firstname) => } - - - - - {(lastname) => } - -
; - } + + + {(lastname) => } + +
; } diff --git a/packages/dataparcels-docs/src/examples/ModifyConditional.jsx b/packages/dataparcels-docs/src/examples/ModifyConditional.jsx deleted file mode 100644 index 40c6642a..00000000 --- a/packages/dataparcels-docs/src/examples/ModifyConditional.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const AgeParcelHoc = ParcelHoc({ - name: "ageParcel", - valueFromProps: (/* props */) => 22 -}); - -const NameEditor = (props) => { - let ageParcel = props - .ageParcel - .modifyDown(number => `${number}`) - // .modifyShapeUp(parcelShape => { - // let updated = parcelShape.update(string => Number(string)); - // return isNaN(updated.value) ? undefined : updated; - // }); - // .modifyChange((parcel, changeRequest) => { - // let string = changeRequest.nextData.value; - // let updated = Number(string); - // if(!isNaN(updated)) { - // parcel.set(updated); - // } - // }) - - return
- - - {(ageParcel) => } - -
; -}; - -export default AgeParcelHoc(ExampleHoc(NameEditor)); diff --git a/packages/dataparcels-docs/src/examples/ParcelBoundaryDebounce.jsx b/packages/dataparcels-docs/src/examples/ParcelBoundaryDebounce.jsx index b4bf4793..ac580ad5 100644 --- a/packages/dataparcels-docs/src/examples/ParcelBoundaryDebounce.jsx +++ b/packages/dataparcels-docs/src/examples/ParcelBoundaryDebounce.jsx @@ -1,19 +1,18 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const FoodParcelHoc = ParcelHoc({ - name: "foodParcel", - valueFromProps: (/* props */) => ({ - mains: "Soup", - dessert: "Strudel" - }) -}); +export default function FoodEditor(props) { -const FoodEditor = (props) => { - let {foodParcel} = props; - return
+ let [foodParcel] = useParcelState({ + value: { + mains: "Soup", + dessert: "Strudel" + } + }); + + return exampleFrame({foodParcel},
{(mains) => } @@ -23,7 +22,5 @@ const FoodEditor = (props) => { {(dessert) => } -
; -}; - -export default FoodParcelHoc(ExampleHoc(FoodEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/ParcelBoundaryForceUpdate.jsx b/packages/dataparcels-docs/src/examples/ParcelBoundaryForceUpdate.jsx deleted file mode 100644 index f16e242f..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelBoundaryForceUpdate.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const ColourParcelHoc = ParcelHoc({ - name: "colourParcel", - valueFromProps: (/* props */) => "Option A" -}); - -const ColourEditor = (props) => { - let {colourParcel, options} = props; - return
- - - {(mains) => } - -
; -}; - -const FoceUpdateExample = ColourParcelHoc(ExampleHoc(ColourEditor)); - -export default class ParcelHocDelayUntilExample extends React.Component { - constructor(props) { - super(props); - this.state = { - options: [] - }; - setTimeout(() => { - this.setState({ - options: [ - {label: "Option A", value: "Option A"}, - {label: "Option B", value: "Option B"} - ] - }); - }, 1000); - } - - render() { - let {options} = this.state; - return ; - } -} diff --git a/packages/dataparcels-docs/src/examples/ParcelBoundaryHold.jsx b/packages/dataparcels-docs/src/examples/ParcelBoundaryHold.jsx deleted file mode 100644 index 279cfea6..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelBoundaryHold.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const NameParcelHoc = ParcelHoc({ - name: "nameParcel", - valueFromProps: (/* props */) => "Gregor" -}); - -const NameEditor = (props) => { - let {nameParcel} = props; - return
- - - {(nameParcel, {release, cancel}) =>
- - - -
} -
-
; -}; - -export default NameParcelHoc(ExampleHoc(NameEditor)); diff --git a/packages/dataparcels-docs/src/examples/ParcelBoundaryPure.jsx b/packages/dataparcels-docs/src/examples/ParcelBoundaryPure.jsx index b7d4979a..b31af82b 100644 --- a/packages/dataparcels-docs/src/examples/ParcelBoundaryPure.jsx +++ b/packages/dataparcels-docs/src/examples/ParcelBoundaryPure.jsx @@ -1,19 +1,7 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - name: { - first: "Robert", - last: "Clamps" - }, - age: "33", - height: "160" - }) -}); +import exampleFrame from 'component/exampleFrame'; const DebugRender = ({children}) => { // each render, have a new, random background colour @@ -26,9 +14,20 @@ const DebugRender = ({children}) => { return
{children}
; }; -const PersonEditor = (props) => { - let {personParcel} = props; - return
+export default function PersonEditor(props) { + + let [personParcel] = useParcelState({ + value: { + name: { + first: "Robert", + last: "Clamps" + }, + age: "33", + height: "160" + } + }); + + return exampleFrame({personParcel},
{(name) => @@ -55,7 +54,5 @@ const PersonEditor = (props) => { } -
; -}; - -export default PersonParcelHoc(ExampleHoc(PersonEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/ParcelHocExample.jsx b/packages/dataparcels-docs/src/examples/ParcelHocExample.jsx deleted file mode 100644 index 6d710503..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocExample.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: () => "word" -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -export default WordParcelHoc(ExampleHoc(WordEditor)); diff --git a/packages/dataparcels-docs/src/examples/ParcelHocExampleDelayUntil.jsx b/packages/dataparcels-docs/src/examples/ParcelHocExampleDelayUntil.jsx deleted file mode 100644 index b73a3ebd..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocExampleDelayUntil.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; - -const DelayParcelHoc = ParcelHoc({ - name: "delayParcel", - valueFromProps: (props) => `Created at ${props.seconds} seconds!`, - delayUntil: (props) => props.seconds > 3 -}); - -const DelayEditor = (props) => { - let {delayParcel, seconds} = props; - - let input = delayParcel - ? - :

No parcel yet...

; - - return
-

Seconds: {seconds}

- {input} -
; -}; - -const DelayExample = DelayParcelHoc(ExampleHoc(DelayEditor)); - -export default class ParcelHocDelayUntilExample extends React.Component { - constructor(props) { - super(props); - this.state = { - seconds: 0 - }; - this.interval = setInterval(() => { - this.setState(({seconds}) => ({ - seconds: seconds + 1 - })); - }, 1000); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - - render() { - let {seconds} = this.state; - return ; - } -} diff --git a/packages/dataparcels-docs/src/examples/ParcelHocExampleInitialValueFromProps.jsx b/packages/dataparcels-docs/src/examples/ParcelHocExampleInitialValueFromProps.jsx deleted file mode 100644 index 2b92672b..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocExampleInitialValueFromProps.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (props) => props.initialWord -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -const WordExample = WordParcelHoc(ExampleHoc(WordEditor)); - -export default (/* props */) => { - return ; -}; diff --git a/packages/dataparcels-docs/src/examples/ParcelHocExampleOnChange.jsx b/packages/dataparcels-docs/src/examples/ParcelHocExampleOnChange.jsx deleted file mode 100644 index 2f909c42..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocExampleOnChange.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (props) => props.defaultValue, - onChange: (props) => (value) => props.onChange(value) -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -const WordExample = WordParcelHoc(ExampleHoc(WordEditor)); - -export default (/* props */) => { - let onChange = (value) => console.log(value); - return ; -}; diff --git a/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromProps.jsx b/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromProps.jsx deleted file mode 100644 index 3c066b3e..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromProps.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ExampleHoc from 'component/ExampleHoc'; -import {Box} from 'dcme-style'; - -const NameParcelHoc = ParcelHoc({ - name: "nameParcel", - valueFromProps: (props) => props.name, - onChange: (props) => (value) => props.onChangeName(value), - shouldParcelUpdateFromProps: (prevProps, nextProps, valueFromProps) => { - return valueFromProps(prevProps) !== valueFromProps(nextProps); - } -}); - -const NameEditor = (props) => { - let {nameParcel} = props; - return
- - -
; -}; - -const UpdateFromPropsExample = NameParcelHoc(ExampleHoc(NameEditor)); - -export default class ParcelHocUpdateFromPropsExample extends React.Component { - - state = { - name: "George" - }; - - render() { - let {name} = this.state; - let onChangeName = (newName) => this.setState({ - name: newName - }); - - return - -

Higher-up state: {name}

- - onChangeName(e.currentTarget.value)} /> - - - -
-
; - } -} diff --git a/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromQueryString.jsx b/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromQueryString.jsx deleted file mode 100644 index 95541a52..00000000 --- a/packages/dataparcels-docs/src/examples/ParcelHocUpdateFromQueryString.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; -import IsRenderingStaticHtml from 'utils/IsRenderingStaticHtml'; -import ReactRouterQueryStringHoc from 'react-cool-storage/lib/ReactRouterQueryStringHoc'; - -import composeWith from 'unmutable/lib/util/composeWith'; - -const QueryStringParcelHoc = ParcelHoc({ - name: "queryStringParcel", - valueFromProps: (props) => props.queryString.value, - onChange: (props) => props.queryString.onChange, - shouldParcelUpdateFromProps: (prevValue, nextValue, valueFromProps) => { - return valueFromProps(prevValue) !== valueFromProps(nextValue); - } -}); - -const QueryStringEditor = (props) => { - let {queryStringParcel} = props; - return
- - - {(parcel) => } - - - - - {(parcel) => } - -
; -}; - -export default composeWith( - ReactRouterQueryStringHoc({ - name: "queryString", - silent: IsRenderingStaticHtml() // gatsby-specific config to render static html without required globals - }), - QueryStringParcelHoc, - ExampleHoc, - QueryStringEditor -); diff --git a/packages/dataparcels-docs/src/examples/ParcelMetaConfirmingDeletions.jsx b/packages/dataparcels-docs/src/examples/ParcelMetaConfirmingDeletions.jsx index f1852c1c..71716046 100644 --- a/packages/dataparcels-docs/src/examples/ParcelMetaConfirmingDeletions.jsx +++ b/packages/dataparcels-docs/src/examples/ParcelMetaConfirmingDeletions.jsx @@ -1,20 +1,19 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +export default function FruitListEditor(props) { -const FruitListEditor = (props) => { - let {fruitListParcel} = props; - return
+ let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + + return exampleFrame({fruitListParcel},
{fruitListParcel.toArray((fruitParcel) => { return {(parcel) =>
@@ -29,7 +28,5 @@ const FruitListEditor = (props) => { ; })} -
; -}; - -export default FruitListParcelHoc(ExampleHoc(FruitListEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/ParcelMetaSelections.jsx b/packages/dataparcels-docs/src/examples/ParcelMetaSelections.jsx index 588b5490..49424b23 100644 --- a/packages/dataparcels-docs/src/examples/ParcelMetaSelections.jsx +++ b/packages/dataparcels-docs/src/examples/ParcelMetaSelections.jsx @@ -1,31 +1,36 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import shape from 'react-dataparcels/shape'; -import ExampleHoc from 'component/ExampleHoc'; +import exampleFrame from 'component/exampleFrame'; -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +export default function FruitListEditor(props) { -const FruitListEditor = (props) => { - let {fruitListParcel} = props; + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); let selectedFruit = fruitListParcel .toArray() .filter(fruit => fruit.meta.selected); let allSelected = fruitListParcel.value.length === selectedFruit.length; + let selectAll = (selected) => fruitListParcel.map(shape( fruit => fruit.setMeta({selected}) )); - return
+ let deleteSelectedFruit = () => fruitListParcel.update(shape( + fruitListShape => fruitListShape + .toArray() + .filter(fruitShape => !fruitShape.meta.selected) + )); + + return exampleFrame({fruitListParcel},
{fruitListParcel.toArray((fruitParcel) => { return {(parcel) => { @@ -45,6 +50,8 @@ const FruitListEditor = (props) => { ? : } + +

Selected fruit:

    {selectedFruit.map((fruitParcel) => { @@ -54,7 +61,5 @@ const FruitListEditor = (props) => { ; })}
-
; -}; - -export default FruitListParcelHoc(ExampleHoc(FruitListEditor)); +
); +} diff --git a/packages/dataparcels-docs/src/examples/SubmitButton.jsx b/packages/dataparcels-docs/src/examples/SubmitButton.jsx index 18dc63e9..429b056c 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButton.jsx +++ b/packages/dataparcels-docs/src/examples/SubmitButton.jsx @@ -1,13 +1,24 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; -import ExampleHoc from 'component/ExampleHoc'; -import composeWith from 'unmutable/composeWith'; +import exampleFrame from 'component/exampleFrame'; -const PersonEditor = (props) => { - let {personParcel, personParcelControl} = props; - return
+export default function PersonEditor(props) { + + let [personParcelState] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps" + } + }); + + let [personParcel, personParcelBuffer] = useParcelBuffer({ + parcel: personParcelState, + hold: true + }); + + return exampleFrame({personParcelState, personParcel},
{(firstname) => } @@ -18,25 +29,7 @@ const PersonEditor = (props) => { {(lastname) => } - - -
; -}; - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) - -export default composeWith( - ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps" - }) - }), - ExampleHoc, - ParcelBoundaryHoc({ - name: "personParcel", - hold: true // hold onto changes until the user releases them - }), - PersonEditor -); + + +
); +} diff --git a/packages/dataparcels-docs/src/examples/ValidationExample.jsx b/packages/dataparcels-docs/src/examples/ValidationExample.jsx index 6cdc4b35..dddfc040 100644 --- a/packages/dataparcels-docs/src/examples/ValidationExample.jsx +++ b/packages/dataparcels-docs/src/examples/ValidationExample.jsx @@ -1,25 +1,60 @@ import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; import Validation from 'react-dataparcels/Validation'; -import ExampleHoc from 'component/ExampleHoc'; -import composeWith from 'unmutable/composeWith'; +import exampleFrame from 'component/exampleFrame'; const numberToString = (parcel) => parcel .modifyDown(number => `${number}`) .modifyUp(string => Number(string)); +const validateStringNotBlank = (name) => (value) => { + return (!value || value.trim().length === 0) && `${name} must not be blank`; +}; + +const validateInteger = (name) => (value) => { + return !Number.isInteger(value) && `${name} must be a whole number`; +}; + +const validatePositiveNumber = (name) => (value) => { + return value < 0 && `${name} must not be negative`; +}; + +const validation = Validation({ + 'name': validateStringNotBlank("Name"), + 'animals.*.type': validateStringNotBlank("Animal type"), + 'animals.*.amount': [ + validateInteger("Animal amount"), + validatePositiveNumber("Animal amount") + ] +}); + const InputWithError = (parcel) =>
{parcel.meta.invalid &&
Error: {parcel.meta.invalid}
}
; -const AnimalEditor = (props) => { - let {animalParcel, animalParcelControl} = props; +export default function AnimalEditor(props) { + + let [animalParcelState] = useParcelState({ + value: { + name: "Robert Clamps", + animals: [ + {type: "Sheep", amount: 6} + ] + } + }); + + let [animalParcel, animalParcelBuffer] = useParcelBuffer({ + parcel: animalParcelState, + hold: true, + beforeChange: validation.beforeChange + }); + let {valid} = animalParcel.meta; - return
+ return exampleFrame({animalParcelState, animalParcel},
{InputWithError} @@ -48,49 +83,7 @@ const AnimalEditor = (props) => {
- - -
; -}; - -const validateStringNotBlank = (name) => (value) => { - return (!value || value.trim().length === 0) && `${name} must not be blank`; -}; - -const validateInteger = (name) => (value) => { - return !Number.isInteger(value) && `${name} must be a whole number`; -}; - -const validatePositiveNumber = (name) => (value) => { - return value < 0 && `${name} must not be negative`; -}; - -const validation = Validation({ - 'name': validateStringNotBlank("Name"), - 'animals.*.type': validateStringNotBlank("Animal type"), - 'animals.*.amount': [ - validateInteger("Animal amount"), - validatePositiveNumber("Animal amount") - ] -}); - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) - -export default composeWith( - ParcelHoc({ - name: "animalParcel", - valueFromProps: (/* props */) => ({ - name: "Robert Clamps", - animals: [ - {type: "Sheep", amount: 6} - ] - }) - }), - ExampleHoc, - ParcelBoundaryHoc({ - name: "animalParcel", - hold: true, - modifyBeforeUpdate: [validation.modifyBeforeUpdate] - }), - AnimalEditor -); + + +
); +} diff --git a/packages/dataparcels-docs/src/pages/api/ParcelBoundary.jsx b/packages/dataparcels-docs/src/pages/api/ParcelBoundary.jsx index 5c5f77ae..2c349cdd 100644 --- a/packages/dataparcels-docs/src/pages/api/ParcelBoundary.jsx +++ b/packages/dataparcels-docs/src/pages/api/ParcelBoundary.jsx @@ -6,16 +6,12 @@ import Markdown_ParcelBoundary from 'docs/api/parcelBoundary/ParcelBoundary.md'; import Markdown_ParcelBoundaryAfter from 'docs/api/parcelBoundary/ParcelBoundaryAfter.md'; import Markdown_childRenderer from 'docs/api/parcelBoundary/childRenderer.md'; import Markdown_parcel from 'docs/api/parcelBoundary/parcel.md'; +import Markdown_pure from 'docs/api/parcelBoundary/pure.md'; import Markdown_forceUpdate from 'docs/api/parcelBoundary/forceUpdate.md'; import Markdown_debounce from 'docs/api/parcelBoundary/debounce.md'; import Markdown_hold from 'docs/api/parcelBoundary/hold.md'; -import Markdown_pure from 'docs/api/parcelBoundary/pure.md'; -import Markdown_modifyBeforeUpdate from 'docs/api/parcelBoundary/modifyBeforeUpdate.md'; +import Markdown_beforeChange from 'docs/api/parcelBoundary/beforeChange.md'; import Markdown_keepValue from 'docs/api/parcelBoundary/keepValue.md'; -import Markdown_onCancel from 'docs/api/parcelBoundary/onCancel.md'; -import Markdown_onRelease from 'docs/api/parcelBoundary/onRelease.md'; -import Markdown_debugBuffer from 'docs/api/parcelBoundary/debugBuffer.md'; -import Markdown_debugParcel from 'docs/api/parcelBoundary/debugParcel.md'; import Layout from 'layouts/Layout'; const md = { @@ -23,16 +19,12 @@ const md = { _after: Markdown_ParcelBoundaryAfter, childRenderer: Markdown_childRenderer, parcel: Markdown_parcel, + pure: Markdown_pure, forceUpdate: Markdown_forceUpdate, debounce: Markdown_debounce, hold: Markdown_hold, - modifyBeforeUpdate: Markdown_modifyBeforeUpdate, - pure: Markdown_pure, - keepValue: Markdown_keepValue, - onCancel: Markdown_onCancel, - onRelease: Markdown_onRelease, - debugBuffer: Markdown_debugBuffer, - debugParcel: Markdown_debugParcel + beforeChange: Markdown_beforeChange, + keepValue: Markdown_keepValue } const api = ` @@ -41,16 +33,12 @@ childRenderer # Props parcel -debounce pure forceUpdate +debounce hold -modifyBeforeUpdate +beforeChange keepValue -onCancel -onRelease -debugBuffer -debugParcel `; export default () => diff --git a/packages/dataparcels-docs/src/pages/api/ParcelBoundaryHoc.jsx b/packages/dataparcels-docs/src/pages/api/ParcelBoundaryHoc.jsx index 491c969b..c0e6e1e8 100644 --- a/packages/dataparcels-docs/src/pages/api/ParcelBoundaryHoc.jsx +++ b/packages/dataparcels-docs/src/pages/api/ParcelBoundaryHoc.jsx @@ -6,11 +6,7 @@ import Markdown_ParcelBoundaryHoc from 'docs/api/parcelBoundaryHoc/ParcelBoundar import Markdown_name from 'docs/api/parcelBoundaryHoc/name.md'; import Markdown_debounce from 'docs/api/parcelBoundaryHoc/debounce.md'; import Markdown_hold from 'docs/api/parcelBoundaryHoc/hold.md'; -import Markdown_modifyBeforeUpdate from 'docs/api/parcelBoundaryHoc/modifyBeforeUpdate.md'; -import Markdown_debugBuffer from 'docs/api/parcelBoundaryHoc/debugBuffer.md'; -import Markdown_debugParcel from 'docs/api/parcelBoundaryHoc/debugParcel.md'; -import Markdown_onCancel from 'docs/api/parcelBoundaryHoc/onCancel.md'; -import Markdown_onRelease from 'docs/api/parcelBoundaryHoc/onRelease.md'; +import Markdown_beforeChange from 'docs/api/parcelBoundaryHoc/beforeChange.md'; import Markdown_childName from 'docs/api/parcelBoundaryHoc/childName.md'; import Markdown_childNameControl from 'docs/api/parcelBoundaryHoc/childNameControl.md'; import Layout from 'layouts/Layout'; @@ -20,11 +16,7 @@ const md = { name: Markdown_name, debounce: Markdown_debounce, hold: Markdown_hold, - modifyBeforeUpdate: Markdown_modifyBeforeUpdate, - debugBuffer: Markdown_debugBuffer, - debugParcel: Markdown_debugParcel, - onCancel: Markdown_onCancel, - onRelease: Markdown_onRelease, + beforeChange: Markdown_beforeChange, ['${name}']: Markdown_childName, ['${name}Control']: Markdown_childNameControl } @@ -34,11 +26,7 @@ const api = ` name debounce hold -modifyBeforeUpdate -onCancel -onRelease -debugBuffer -debugParcel +beforeChange # Child props $\{name\} diff --git a/packages/dataparcels-docs/src/pages/data-editing.md b/packages/dataparcels-docs/src/pages/data-editing.md index 62b54adf..53e48fca 100644 --- a/packages/dataparcels-docs/src/pages/data-editing.md +++ b/packages/dataparcels-docs/src/pages/data-editing.md @@ -36,22 +36,21 @@ This example demonstrates a pretty typical React setup to do that. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps", - address: { - postcode: "1234" +export default function PersonEditor(props) { + + let [personParcel] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps", + address: { + postcode: "1234" + } } - }) -}); + }); -const PersonEditor = (props) => { - let {personParcel} = props; return
@@ -70,8 +69,6 @@ const PersonEditor = (props) => {
; }; -export default PersonParcelHoc(PersonEditor); - ``` ### What's going on @@ -92,27 +89,25 @@ Dataparcels has a powerful set of methods for manipulating indexed data types, s Notice how items in the array are given **automatic unique keys**, displayed under each input as `#a`, `#b` ..., which can be used by React to identify each element regardless of how the elements move around. -Make sure you check out the drag and drop sorting example too. +Make sure you check out the drag and drop sorting example too. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); -const FruitListEditor = (props) => { - let {fruitListParcel} = props; +export default function FruitListEditor(props) { + + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + return
{fruitListParcel.toArray((fruitParcel) => { return @@ -122,15 +117,14 @@ const FruitListEditor = (props) => { - key {fruitParcel.key} + key {fruitParcel.key}
}
; })} ; -}; +} -export default FruitListParcelHoc(FruitListEditor); ``` ### What's going on @@ -152,15 +146,10 @@ Sometimes you may hit a situation where a Parcel contains data you want to be ab ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const AlphanumericParcelHoc = ParcelHoc({ - name: "alphanumericParcel", - valueFromProps: (/* props */) => "Abc123" -}); - -const AlphanumericInput = (props) => { +function AlphanumericInput(props) { return {(alphanumericParcel) => { let parcel = alphanumericParcel.modifyUp(string => string.replace(/[^a-zA-Z0-9]/g, "")); @@ -168,18 +157,20 @@ const AlphanumericInput = (props) => { return ; }} ; -}; +} + +export default function AlphanumericEditor(props) { + + let [alphanumericParcel] = useParcelState({ + value: "Abc123" + }); -const AlphanumericEditor = (props) => { - let {alphanumericParcel} = props; return

Alphanumeric input

Disallows all non-alphanumeric characters. Try typing some punctuation.

; -}; - -export default AlphanumericParcelHoc(AlphanumericEditor); +} ``` @@ -187,15 +178,10 @@ export default AlphanumericParcelHoc(AlphanumericEditor); ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const NumberParcelHoc = ParcelHoc({ - name: "numberParcel", - valueFromProps: (/* props */) => 123 -}); - -const NumberInput = (props) => { +function NumberInput(props) { let numberParcel = props .numberParcel .modifyUp(string => Number(string)) @@ -204,42 +190,42 @@ const NumberInput = (props) => { // ^ turn value into a string on the way down // and turn value back into a number on the way up - // without the keepValue prop, typing "0.10" - // would immediately be replaced with "0.1" - // as the new value is turned into a number on the way up, - // and into a string on the way down - // which would make typing very frustrating + // *the keepValue prop is necessary here, see note below return {(parcel) => } ; -}; +} + +export default function NumberEditor(props) { + + let [numberParcel] = useParcelState({ + value: 123 + }); -const NumberEditor = (props) => { - let {numberParcel} = props; return

Number > string

-

Turns a stored number into a string for editing

+

Turns a stored number into a string for editing.

; -}; - -export default NumberParcelHoc(NumberEditor); +} ``` +#### The keepValue prop + +The `keepValue` prop is necessary here to allow the ParcelBoundary to be the master of its own state. +So even when a non-number is entered into the input (e.g. "A"), and this is turned into `NaN` as it passes through `.modifyUp()`, the ParcelBoundary can still remember that it should contain "A". + +See [ParcelBoundary.keepValue](/api/ParcelBoundary#keepValue) for more details. + ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const DelimitedStringParcelHoc = ParcelHoc({ - name: "delimitedParcel", - valueFromProps: (/* props */) => "abc.def" -}); - -const DelimitedStringInput = (props) => { +function DelimitedStringInput(props) { let delimitedStringParcel = props .delimitedStringParcel .modifyDown(string => string.split(".")) @@ -260,18 +246,21 @@ const DelimitedStringInput = (props) => { })} ; -}; +} + +export default function DelimitedStringEditor(props) { + + let [delimitedParcel] = useParcelState({ + value: "abc.def" + }); -const DelimitedStringEditor = (props) => { - let {delimitedParcel} = props; return

Delimited string > array of strings

Turns a stored string into an array so array editing controls can be rendered.

; -}; +} -export default DelimitedStringParcelHoc(DelimitedStringEditor); ``` @@ -319,90 +308,77 @@ export default MaybeArrayParcelHoc(MaybeArrayEditor); ## Derived data -It's easy to update Parcel data based on other Parcel data using `modifyBeforeUpdate` available on ParcelHoc, ParcelBoundary and ParcelBoundaryHoc. +It's easy to update Parcel data based on other Parcel data using `beforeChange` available on [useParcelState](/api/useParcelState#beforeChange), [useParcelBuffer](/api/useParcelBuffer#beforeChange) and [ParcelBoundary](/api/ParcelBoundary#beforeChange). -It works quite like modifyUp() as shown in Modifying data to fit the UI, but `modifyBeforeUpdate` is also applied to the initial value *and* any updates that occur because of prop changes. +It works quite like modifyUp() as shown in Modifying data to fit the UI, but `beforeChange` is also applied to the initial value *and* any updates that occur because of prop changes. -This example derives an uppercase version of the word. +This example derives the length of the word. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (/* props */) => ({ - word: "blueberries", - uppercase: undefined - }), - modifyBeforeUpdate: [ - (value) => ({ +export default function WordEditor(props) { + + let [wordParcel] = useParcelState({ + value: { + word: "blueberries", + wordLength: undefined + }, + beforeChange: (value) => ({ word: value.word, - uppercase: value.word.toUpperCase() + wordLength: value.word.length }) - ] -}); + }); -const WordEditor = (props) => { - let {wordParcel} = props; return
{(parcel) => } -

Uppercase word is {wordParcel.get('uppercase').value}

+

word length is {wordParcel.get('wordLength').value}

; -}; - -export default WordParcelHoc(WordEditor); +} ``` -Setting derived data is particularly useful with Parcel meta, which provides the ability to store extra data that pertains to parts of a data shape. +A potential problem with the above example is that it stores derived data in the parcel's *value*. Perhaps the data shape you're editing can not or should not have a new field added to it. It is this reason that [Parcel meta](/parcel-meta) exists, which provides a convenient place to store extra data that pertains to parts of a data shape. -This example derives the length of the word, storing it in meta. It also uses a shape updater. +This example also derives the length of the word, but this time it stores it in meta. It also uses a [shape updater](/api/ParcelShape), an advanced editing feature of dataparcels. The shape updater's syntax can look a little strange, but it allows for powerful manipulations of the shape of a value. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import shape from 'react-dataparcels/shape'; -// this example uses a shape updater to set meta data -const setWordLengthMeta = shape(parcelShape => { - let word = parcelShape.value; - return parcelShape.setMeta({ - wordLength: word.length - }); -}); +const setWordLengthMeta = shape(parcelShape => parcelShape.setMeta({ + wordLength: parcelShape.value.word.length +})); -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (/* props */) => "blueberries", - modifyBeforeUpdate: [ - setWordLengthMeta - ] -}); +export default function WordEditor(props) { + + let [wordParcel] = useParcelState({ + value: { + word: "blueberries", + wordLength: undefined + }, + beforeChange: setWordLengthMeta + }); -const WordEditor = (props) => { - let {wordParcel} = props; return
- - {(parcel) =>
- -

length is {parcel.meta.wordLength}

-
} + + {(parcel) => } +

word length is {wordParcel.meta.wordLength}

; -}; - -export default WordParcelHoc(WordEditor); +} ``` @@ -410,7 +386,7 @@ export default WordParcelHoc(WordEditor); ## Fields that interact with each other -Some forms contain fields that influence each other's values. Dataparcels can manage this through the use of `modifyBeforeUpdate`. +Some forms contain fields that influence each other's values. Dataparcels can manage this through the use of `beforeChange`. This example sums `a` and `b` together. If `a` or `b` are edited, then `sum = a + b`. If `sum` is edited, `a` and `b` are scaled appropriately so they remain proportional to one another. @@ -419,11 +395,11 @@ If `sum` is edited, `a` and `b` are scaled appropriately so they remain proporti ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import CancelActionMarker from 'react-dataparcels/CancelActionMarker'; -const calculate = (value, changeRequest) => { +function calculate(value, changeRequest) { let {a, b, sum} = value; if(changeRequest.originPath[0] !== "sum") { @@ -446,19 +422,7 @@ const calculate = (value, changeRequest) => { } return {a, b, sum}; -}; - -const SumParcelHoc = ParcelHoc({ - name: "sumParcel", - valueFromProps: (/* props */) => ({ - a: 5, - b: 5, - sum: undefined - }), - modifyBeforeUpdate: [ - calculate - ] -}); +} // turn numbers into strings on the way down // and back into numbers on the way up @@ -474,8 +438,17 @@ const numberToString = (parcel) => parcel return (string === "" || isNaN(number)) ? CancelActionMarker : number; }); -const AreaEditor = (props) => { - let {sumParcel} = props; +export default function AreaEditor(props) { + + let [sumParcel] = useParcelState({ + value: { + a: 5, + b: 5, + sum: undefined + }, + beforeChange: calculate + }); + return
@@ -492,9 +465,8 @@ const AreaEditor = (props) => { {(parcel) => }
; -}; +} -export default SumParcelHoc(AreaEditor); ``` @@ -510,38 +482,33 @@ This example also serves as an indication on how you might use `dataparcels` wit ```js import React from 'react'; +import {useState} from 'react'; import Parcel from 'react-dataparcels'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -export default class ManagingOwnParcelState extends React.Component { - constructor(props) { - super(props); +export default function ManagingOwnParcelState(props) { - let personParcel = new Parcel({ - value: { - firstname: "Robert", - lastname: "Clamps" - }, - handleChange: (personParcel) => this.setState({personParcel}) - }); + let [personParcel, setPersonParcel] = useState(() => new Parcel({ + value: { + firstname: "Robert", + lastname: "Clamps" + }, + handleChange: (parcel) => { + setPersonParcel(parcel); + } + })); - this.state = {personParcel}; - } + return
+ + + {(firstname) => } + - render() { - let {personParcel} = this.state; - return
- - - {(firstname) => } - - - - - {(lastname) => } - -
; - } + + + {(lastname) => } + +
; } ``` diff --git a/packages/dataparcels-docs/src/pages/examples/parcelboundary-forceUpdate.js b/packages/dataparcels-docs/src/pages/examples/parcelboundary-forceUpdate.js deleted file mode 100644 index 9b597752..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelboundary-forceUpdate.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelboundary-forceUpdate.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelboundary-hold.js b/packages/dataparcels-docs/src/pages/examples/parcelboundary-hold.js deleted file mode 100644 index 4552ed2d..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelboundary-hold.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelboundary-hold.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.js b/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.js deleted file mode 100644 index edab946d..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelhoc-delayuntil.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.md b/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.md deleted file mode 100644 index 04efb354..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-delayuntil.md +++ /dev/null @@ -1,55 +0,0 @@ -import Link from 'gatsby-link'; -import ParcelHocExampleDelayUntil from 'examples/ParcelHocExampleDelayUntil'; - -This example shows how to delay the creation of a Parcel with `ParcelHoc`. The example counts the number of seconds that have passed, and passes this into the `DelayExample` component. The `ParcelHoc` is set to delay until seconds > 3. Once seconds = 4, `valueFromProps` is called and the Parcel is created. - -API reference for ParcelHoc.delayUntil - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; - -const DelayParcelHoc = ParcelHoc({ - name: "delayParcel", - valueFromProps: (props) => `Created at ${props.seconds} seconds!`, - delayUntil: (props) => props.seconds > 3 -}); - -const DelayEditor = (props) => { - let {delayParcel, seconds} = props; - - let input = delayParcel - ? - :

No parcel yet...

; - - return
-

Seconds: {seconds}

- {input} -
; -}; - -const DelayExample = DelayParcelHoc(DelayEditor); - -export default class ParcelHocDelayUntilExample extends React.Component { - constructor(props) { - super(props); - this.state = { - seconds: 0 - }; - setInterval(() => { - this.setState(({seconds}) => ({ - seconds: seconds + 1 - })); - }, 1000); - } - - render() { - let {seconds} = this.state; - return ; - } -} - - -``` diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.js b/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.js deleted file mode 100644 index 998ad894..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelhoc-example.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.md b/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.md deleted file mode 100644 index a8578bfd..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-example.md +++ /dev/null @@ -1,30 +0,0 @@ -import Link from 'gatsby-link'; -import ParcelHocExample from 'examples/ParcelHocExample'; - -This example demonstrates a simple usage of `ParcelHoc`. - -API reference for ParcelHoc - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: () => "word" -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -export default WordParcelHoc(WordEditor); -``` - -### What's going on - -* When `ParcelHoc` mounts, it calls `valueFromProps` and puts the result ("word") into its Parcel. -* `wordParcel` is passed to `WordEditor` for editing. Changes to `wordParcel` are stored in `ParcelHoc`s state. diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.js b/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.js deleted file mode 100644 index 0515bdcd..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelhoc-onchange.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.md b/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.md deleted file mode 100644 index 358d41e1..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-onchange.md +++ /dev/null @@ -1,43 +0,0 @@ -import Link from 'gatsby-link'; -import ParcelHocExampleOnChange from 'examples/ParcelHocExampleOnChange'; - -This example demonstrates a `ParcelHoc` with an initial value that originates from props, and a `onChange` function that logs out each change to the console. - -`ParcelHoc.onChange` is often used to relay changes further up the React DOM heirarchy. This works in a very similar way to [uncontrolled components in React](https://reactjs.org/docs/uncontrolled-components.html), in that **the component holds the state and is the source of truth**. The components above that make use of the `onChange` props are can merely respond to those changes. - -API reference for ParcelHoc.onChange - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (props) => props.defaultValue, - onChange: (props) => (value) => props.onChange(value) -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -const WordExample = WordParcelHoc(WordEditor); - -export default (/* props */) => { - let onChange = (value) => console.log(value); - return ; -}; - -``` - -### What's going on - -* `WordExample` passes down an initial `word` prop. -* When `ParcelHoc` mounts, it calls `valueFromProps` and puts the result ("word") into its Parcel. - - From this point forward, ParcelHoc is the source of truth. If `defaultValue` were to change, it would have no effect on `ParcelHoc` or the Parcel's value. -* `wordParcel` is passed to `WordEditor` for editing. Changes to `wordParcel` are stored in `ParcelHoc`s state. -* Additionally **onChange is called each time the Parcel's value changes**. In this example `onChange` then calls `props.onChange`, which logs out the new value to the console. diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.js b/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.js deleted file mode 100644 index 7dfa7452..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelhoc-updatefromprops.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default ({history, location}: *) => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.md b/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.md deleted file mode 100644 index 809906c2..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-updatefromprops.md +++ /dev/null @@ -1,129 +0,0 @@ -import Link from 'gatsby-link'; -import ParcelHocUpdateFromProps from 'examples/ParcelHocUpdateFromProps'; -import ParcelHocUpdateFromQueryString from 'examples/ParcelHocUpdateFromQueryString'; -import {Box, Link as HtmlLink} from 'dcme-style'; -prop history -prop location - -This example shows how `shouldParcelUpdateFromProps` can be used to control the value in a ParcelHoc from changes in props. - -This can be very powerful. You can make a ParcelHoc a slave to state that's held higher up in the React component heirarchy, and still take full advantage of the value editing capabilities that `dataparcels` provides. This also makes it easy to swap out your state storage mechanism with another one later on, without having to refactor any components beneath the ParcelHoc. - -API reference for ParcelHoc.shouldParcelUpdateFromProps - - - -- Updating from higher-up state -- Updating from query string - -## Updating from higher-up state - -This example shows how a ParcelHoc's value can be controlled by external state held in a React component. - -When you type in the first input, the higher-up state is updated, and the changes are passed down and adopted by the ParcelHoc, via `config.shouldParcelUpdateFromProps`. - -When you type in the second input, the ParcelHoc changes and notifies the higher component of the change, via `config.onChange`. The higher component then updates its own state in response to this. - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; - -const NameParcelHoc = ParcelHoc({ - name: "nameParcel", - valueFromProps: (props) => props.name, - onChange: (props) => (value) => props.onChangeName(value), - shouldParcelUpdateFromProps: (prevProps, nextProps, valueFromProps) => { - return valueFromProps(prevProps) !== valueFromProps(nextProps); - } -}); - -const NameEditor = (props) => { - let {nameParcel} = props; - return
- - -
; -}; - -const UpdateFromPropsExample = NameParcelHoc(NameEditor); - -export default class ParcelHocUpdateFromPropsExample extends React.Component { - - state = { - name: "George" - }; - - render() { - let {name} = this.state; - let onChangeName = (newName) => this.setState({ - name: newName - }); - - return
-

Higher-up state: {name}

- - onChangeName(e.currentTarget.value)} /> - -
; - } -} -``` - - - -## Updating from query string - -This example shows how a ParcelHoc's value can be controlled from the query string using [react-router](https://github.com/ReactTraining/react-router) and [react-cool-storage](http://github.com/blueflag/react-cool-storage). Because of `react-cool-storage`, any values representable by JSON can be stored in the query string, not just strings. - -The same method can be used to allow a ParcelHoc to be controlled by another other source of state, such as URL parameters, localStorage or IndexedDB. - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ReactRouterQueryStringHoc from 'react-cool-storage/lib/ReactRouterQueryStringHoc'; - -import composeWith from 'unmutable/lib/util/composeWith'; - -const QueryStringParcelHoc = ParcelHoc({ - name: "queryStringParcel", - valueFromProps: (props) => props.queryString.value, - onChange: (props) => props.queryString.onChange, - shouldParcelUpdateFromProps: (prevProps, nextProps, valueFromProps) => { - return valueFromProps(prevProps) !== valueFromProps(nextProps); - } -}); - -const QueryStringEditor = (props) => { - let {queryStringParcel} = props; - return
- - - {(parcel) => } - - - - - {(parcel) => } - -
; -}; - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) - -export default composeWith( - ReactRouterQueryStringHoc({ - name: "queryString" - }), - QueryStringParcelHoc, - QueryStringEditor -); - -``` diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.js b/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.js deleted file mode 100644 index ca681920..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import React from 'react'; -import Markdown from 'pages/examples/parcelhoc-valuefromprops.md'; -import Example from 'component/Example'; -import Layout from 'layouts/Layout'; - -export default () => - - diff --git a/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.md b/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.md deleted file mode 100644 index eb93416b..00000000 --- a/packages/dataparcels-docs/src/pages/examples/parcelhoc-valuefromprops.md +++ /dev/null @@ -1,37 +0,0 @@ -import Link from 'gatsby-link'; -import ParcelHocExampleInitialValueFromProps from 'examples/ParcelHocExampleInitialValueFromProps'; - -This example demonstrates a `ParcelHoc` with an initial value that originates from props. - -API reference for ParcelHoc.valueFromProps - - - -```js -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; - -const WordParcelHoc = ParcelHoc({ - name: "wordParcel", - valueFromProps: (props) => props.initialWord -}); - -const WordEditor = (props) => { - let {wordParcel} = props; - return ; -}; - -const WordExample = WordParcelHoc(WordEditor); - -export default (/* props */) => { - return ; -}; -``` - -### What's going on - -* `WordExample` passes down an initial `word` prop. -* When `ParcelHoc` mounts, it calls `valueFromProps` and puts the result ("word") into its Parcel. - - **From this point forward, ParcelHoc is the source of truth**. If `initialWord` were to change, it would have no effect on `ParcelHoc` or the Parcel's value. This works in a very similar way to [uncontrolled components in React](https://reactjs.org/docs/uncontrolled-components.html). -* `wordParcel` is passed to `WordEditor` for editing. Changes to `wordParcel` are stored in `ParcelHoc`s state. diff --git a/packages/dataparcels-docs/src/pages/getting-started.md b/packages/dataparcels-docs/src/pages/getting-started.md index eb32d492..12878b58 100644 --- a/packages/dataparcels-docs/src/pages/getting-started.md +++ b/packages/dataparcels-docs/src/pages/getting-started.md @@ -41,25 +41,23 @@ We could do something like this. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; // PLEASE DON'T USE THIS CODE -// THIS CODE IS FOR DEMONSTRAION PURPOSES ONLY +// THIS CODE IS FOR DEMONSTRATION PURPOSES ONLY // THERE'S A BETTER WAY OF DOING IT BELOW -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps", - address: { - postcode: "1234" - } - }) -}); +export default function PersonEditor(props) { -const PersonEditor = (props) => { - let {personParcel} = props; + let [personParcel] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps", + address: { + postcode: "1234" + } + } + }); let firstname = personParcel.get('firstname'); let lastname = personParcel.get('lastname'); @@ -76,8 +74,6 @@ const PersonEditor = (props) => { ; }; - -export default PersonParcelHoc(PersonEditor); ``` ### What's going on @@ -97,22 +93,21 @@ This is the same example with a few improvements added: better rendering perform ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps", - address: { - postcode: "1234" +export default function PersonEditor(props) { + + let [personParcel] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps", + address: { + postcode: "1234" + } } - }) -}); + }); -const PersonEditor = (props) => { - let {personParcel} = props; return
@@ -131,7 +126,6 @@ const PersonEditor = (props) => {
; }; -export default PersonParcelHoc(PersonEditor); ``` @@ -142,5 +136,5 @@ export default PersonParcelHoc(PersonEditor); ## More +- Read more about data editing - Browse the API -- Continue on to data editing diff --git a/packages/dataparcels-docs/src/pages/sandbox.js b/packages/dataparcels-docs/src/pages/sandbox.js index 06fdca41..3c74a9f9 100644 --- a/packages/dataparcels-docs/src/pages/sandbox.js +++ b/packages/dataparcels-docs/src/pages/sandbox.js @@ -1,14 +1,17 @@ // @flow import type {Node} from 'react'; import React from 'react'; -import {Wrapper, Text, Typography} from 'dcme-style'; -import Markdown from 'pages/sandbox.md'; +import {Box, Wrapper, Text, Typography} from 'dcme-style'; import PageLayout from 'component/PageLayout'; import Layout from 'layouts/Layout'; +import ValidationExample from 'examples/ValidationExample'; export default () => } + content={() => + Sandbox + + } /> diff --git a/packages/dataparcels-docs/src/pages/sandbox.md b/packages/dataparcels-docs/src/pages/sandbox.md deleted file mode 100644 index 8a1b05f3..00000000 --- a/packages/dataparcels-docs/src/pages/sandbox.md +++ /dev/null @@ -1,8 +0,0 @@ -import Link from 'gatsby-link'; -import SandboxFrame from 'sandbox/SandboxFrame'; - -# Sandbox - -## Frames - - diff --git a/packages/dataparcels-docs/src/pages/ui-behaviour.md b/packages/dataparcels-docs/src/pages/ui-behaviour.md index 5b7890c6..42f6e88e 100644 --- a/packages/dataparcels-docs/src/pages/ui-behaviour.md +++ b/packages/dataparcels-docs/src/pages/ui-behaviour.md @@ -3,7 +3,6 @@ import SubmitButton from 'examples/SubmitButton'; import ValidationExample from 'examples/ValidationExample'; import Autosave from 'examples/Autosave'; import EditingArraysDrag from 'examples/EditingArraysDrag'; -import EditingArraysFlipMove from 'examples/EditingArraysFlipMove'; import ParcelBoundaryDebounce from 'examples/ParcelBoundaryDebounce'; import ParcelBoundaryPure from 'examples/ParcelBoundaryPure'; import ParcelMetaConfirmingDeletions from 'examples/ParcelMetaConfirmingDeletions'; @@ -18,22 +17,39 @@ UI behaviour covers features that help the user interact with the data. Dataparcels is very often used with data that's fetched from a server, and saved back to a server. When dataparcels is used like this, it's useful to prevent the user's changes from being immediately sent back to the server and instead hold onto them momentarily. We can either wait for the user to choose to send their changes, or wait until an amount of time has passed since the user has made a change, and *then* save the changes to the server. -There is a common pattern to do this using React and Dataparcels, by using multiple higher order components: +There is a common pattern to do this using React and Dataparcels, by using the [useParcelState](/api/useParcelState) and [useParcelBuffer](/api/useParcelBuffer) hooks. ```js -ParcelHoc // holds the data fetched from the server - | // and sends changes to the server - V -ParcelBoundaryHoc // holds the changes that the user has made - | // and momentarily prevents those changes - | // from being propagated back up to the ParcelHoc - V -Editor // allows the user to make changes to the data +function ExampleHooks(props) { + + // 1. Parcel State + // + // holds the original data + // and sends changed data to a callback + let [parcelState] = useParcelState({ + value: props.myData, + onChange: (parcel) => props.saveMyData(parcel.value) + }); + + // 2. Parcel Buffer + // + // buffers the changes that the user has made + // and prevents those changes from being propagated + // back up to state until its ready to be saved + let [parcel] = useParcelBuffer({ + parcel: parcelState, + hold: true // or debounce + }); + + // 3. Editor + // allows the user to make changes to the data + parcel.get('...') // etc +} ``` -Using this pattern, the "submit" button is really an action that instructs a ParcelBoundaryHoc to release all of its buffered changes, allowing them to propagate back up to the ParcelHoc. +Using this pattern, the "submit" button is really an action that instructs the `useParcelBuffer` hook to release all of its buffered changes, allowing them to propagate back up to the `useParcelState` hook. -The examples below show this in action, however in an actual app you would still need to configure the ParcelHoc to send the changes to the server. [Data Synchronisation](/data-synchronisation) describes how that can be done. +The examples below show this in action, however in an actual app you would still need to configure the `useParcelState` hook to send the changes to the server. [Data Synchronisation](/data-synchronisation) describes how that can be done. ### Submit button example @@ -41,13 +57,24 @@ The examples below show this in action, however in an actual app you would still ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; -import composeWith from 'unmutable/composeWith'; -const PersonEditor = (props) => { - let {personParcel, personParcelControl} = props; +export default function PersonEditor(props) { + + let [personParcelState] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps" + } + }); + + let [personParcel, personParcelBuffer] = useParcelBuffer({ + parcel: personParcelState, + hold: true + }); + return
@@ -59,28 +86,11 @@ const PersonEditor = (props) => { {(lastname) => } - - + +
; -}; - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) +} -export default composeWith( - ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps" - }) - }), - ParcelBoundaryHoc({ - name: "personParcel", - hold: true - // ^ hold onto changes until the user releases them - }), - PersonEditor -); ``` ### Autosave example @@ -89,13 +99,24 @@ export default composeWith( ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; -import composeWith from 'unmutable/composeWith'; -const PersonEditor = (props) => { - let {personParcel, personParcelControl} = props; +export default function PersonEditor(props) { + + let [personParcelState] = useParcelState({ + value: { + firstname: "Robert", + lastname: "Clamps" + } + }); + + let [personParcel] = useParcelBuffer({ + parcel: personParcelState, + debounce: 500 // hold onto changes until 500ms have elapsed since last change + }); + return
@@ -107,32 +128,14 @@ const PersonEditor = (props) => { {(lastname) => }
; -}; - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) - -export default composeWith( - ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - firstname: "Robert", - lastname: "Clamps" - }) - }), - ParcelBoundaryHoc({ - name: "personParcel", - debounce: 500 - // ^ hold onto changes until 500ms have elapsed since last change - }), - PersonEditor -); +} ``` ## Validation on user input -Dataparcels' [Validation plugin](/api/Validation) provides an easy way to test whether data conforms to a set of validation rules, show errors to the user, and prevent changes from being released until the data is valid. +Dataparcels' [Validation plugin](/api/Validation) provides an easy way to test whether data conforms to a set of validation rules, show errors to the user, and prevent changes from being submitted until the data is valid. Try removing the value of the `name` field, or choosing a non-numeric or negative value for the amount of animals. @@ -140,23 +143,58 @@ Try removing the value of the `name` field, or choosing a non-numeric or negativ ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; +import useParcelBuffer from 'react-dataparcels/useParcelBuffer'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ParcelBoundaryHoc from 'react-dataparcels/ParcelBoundaryHoc'; import Validation from 'react-dataparcels/Validation'; -import composeWith from 'unmutable/composeWith'; const numberToString = (parcel) => parcel .modifyDown(number => `${number}`) .modifyUp(string => Number(string)); +const validateStringNotBlank = (name) => (value) => { + return (!value || value.trim().length === 0) && `${name} must not be blank`; +}; + +const validateInteger = (name) => (value) => { + return !Number.isInteger(value) && `${name} must be a whole number`; +}; + +const validatePositiveNumber = (name) => (value) => { + return value < 0 && `${name} must not be negative`; +}; + +const validation = Validation({ + 'name': validateStringNotBlank("Name"), + 'animals.*.type': validateStringNotBlank("Animal type"), + 'animals.*.amount': [ + validateInteger("Animal amount"), + validatePositiveNumber("Animal amount") + ] +}); + const InputWithError = (parcel) =>
- {parcel.meta.invalid && `Error: ${parcel.meta.invalid}`} + {parcel.meta.invalid &&
Error: {parcel.meta.invalid}
}
; -const AnimalEditor = (props) => { - let {animalParcel, animalParcelControl} = props; +export default function AnimalEditor(props) { + + let [animalParcelState] = useParcelState({ + value: { + name: "Robert Clamps", + animals: [ + {type: "Sheep", amount: 6} + ] + } + }); + + let [animalParcel, animalParcelBuffer] = useParcelBuffer({ + parcel: animalParcelState, + hold: true, + beforeChange: validation.beforeChange + }); + let {valid} = animalParcel.meta; return
@@ -188,51 +226,10 @@ const AnimalEditor = (props) => {
- - + + ; -}; - -const validateStringNotBlank = (name) => (value) => { - return (!value || value.trim().length === 0) && `${name} must not be blank`; -}; - -const validateInteger = (name) => (value) => { - return !Number.isInteger(value) && `${name} must be a whole number`; -}; - -const validatePositiveNumber = (name) => (value) => { - return value < 0 && `${name} must not be negative`; -}; - -const validation = Validation({ - 'name': validateStringNotBlank("Name"), - 'animals.*.type': validateStringNotBlank("Animal type"), - 'animals.*.amount': [ - validateInteger("Animal amount"), - validatePositiveNumber("Animal amount") - ] -}); - -// unmutable's composeWith(a,b,c) is equivalent to a(b(c)) - -export default composeWith( - ParcelHoc({ - name: "animalParcel", - valueFromProps: (/* props */) => ({ - name: "Robert Clamps", - animals: [ - {type: "Sheep", amount: 6} - ] - }) - }), - ParcelBoundaryHoc({ - name: "animalParcel", - hold: true, - modifyBeforeUpdate: [validation.modifyBeforeUpdate] - }), - AnimalEditor -); +} ``` @@ -248,21 +245,19 @@ This uses [parcel meta](/parcel-meta), a generic way of storing extra data that ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); -const FruitListEditor = (props) => { - let {fruitListParcel} = props; +export default function FruitListEditor(props) { + + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + return
{fruitListParcel.toArray((fruitParcel) => { return @@ -279,9 +274,8 @@ const FruitListEditor = (props) => { })}
; -}; +} -export default FruitListParcelHoc(FruitListEditor); ``` ### What's going on @@ -299,31 +293,36 @@ This example shows how to use meta stored against each element in an array to ke ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import shape from 'react-dataparcels/shape'; -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +export default function FruitListEditor(props) { -const FruitListEditor = (props) => { - let {fruitListParcel} = props; + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); let selectedFruit = fruitListParcel .toArray() .filter(fruit => fruit.meta.selected); let allSelected = fruitListParcel.value.length === selectedFruit.length; + let selectAll = (selected) => fruitListParcel.map(shape( fruit => fruit.setMeta({selected}) )); + let deleteSelectedFruit = () => fruitListParcel.update(shape( + fruitListShape => fruitListShape + .toArray() + .filter(fruitShape => !fruitShape.meta.selected) + )); + return
{fruitListParcel.toArray((fruitParcel) => { return @@ -344,6 +343,8 @@ const FruitListEditor = (props) => { ? : } + +

Selected fruit:

    {selectedFruit.map((fruitParcel) => { @@ -354,9 +355,8 @@ const FruitListEditor = (props) => { })}
; -}; +} -export default FruitListParcelHoc(FruitListEditor); ``` @@ -372,22 +372,14 @@ The `react-dataparcels-drag` hoc attempts to keep a very similar API to `react-s ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; import ParcelDrag from 'react-dataparcels-drag'; - -const FruitListParcelHoc = ParcelHoc({ - name: "fruitListParcel", - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ] -}); +import exampleFrame from 'component/exampleFrame'; const SortableFruitList = ParcelDrag({ element: (fruitParcel) => - {(parcel) =>
+ {(parcel) =>
@@ -395,57 +387,21 @@ const SortableFruitList = ParcelDrag({ }); -const FruitListEditor = (props) => { - let {fruitListParcel} = props; +export default function FruitListEditor(props) { + + let [fruitListParcel] = useParcelState({ + value: [ + "Apple", + "Banana", + "Crumpets" + ] + }); + return
; -}; - -export default FruitListParcelHoc(FruitListEditor); -``` - -### Alternatively, animations with react-flip-move - -Dataparcels' also plays nicely with [react-flip-move](https://github.com/joshwcomeau/react-flip-move) because of its automatic keying. Add, remove and move items to see. - - - -```js -import React from 'react'; -import FlipMove from 'react-flip-move'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; - -const FruitListEditor = (props) => { - let {fruitListParcel} = props; - return - {fruitListParcel.toArray((fruitParcel) => { - return - {(parcel) =>
- - - - - -
} -
; - })} - -
; -}; - -const FruitListParcelHoc = ParcelHoc({ - valueFromProps: (/* props */) => [ - "Apple", - "Banana", - "Crumpets" - ], - name: "fruitListParcel" -}); - -export default FruitListParcelHoc(FruitListEditor); +} ``` @@ -453,7 +409,7 @@ export default FruitListParcelHoc(FruitListEditor); ## Debouncing changes -Debouncing can be used to increase rendering performance for parcels that change value many times in rapid succession, such as text inputs. This feature is available through use of ParcelBoundary or ParcelBoundaryHoc. +Debouncing can be used to increase rendering performance for parcels that change value many times in rapid succession, such as text inputs. This feature is available through use of [ParcelBoundary](/api/ParcelBoundary#debounce) and [useParcelBuffer](/api/useParcelBuffer#debounce). Debouncing can be good for rendering performance because parcels outside the ParcelBoundary don't needlessly update every time a small change occurs (e.g. each time the user presses a key). @@ -461,19 +417,18 @@ Debouncing can be good for rendering performance because parcels outside the Par ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -const FoodParcelHoc = ParcelHoc({ - name: "foodParcel", - valueFromProps: (/* props */) => ({ - mains: "Soup", - dessert: "Strudel" - }) -}); +export default function FoodEditor(props) { + + let [foodParcel] = useParcelState({ + value: { + mains: "Soup", + dessert: "Strudel" + } + }); -const FoodEditor = (props) => { - let {foodParcel} = props; return
@@ -485,9 +440,7 @@ const FoodEditor = (props) => { {(dessert) => }
; -}; - -export default FoodParcelHoc(FoodEditor); +} ``` @@ -495,26 +448,15 @@ export default FoodParcelHoc(FoodEditor); ## Pure rendering -Pure rendering is achieved automatically through the use of ParcelBoundaries. In this example, ParcelBoundaries render as coloured boxes. As you type in an input, the colours will change to indicate which ParcelBoundaries have re-rendered. +Pure rendering is achieved automatically through the use of [ParcelBoundaries](/api/ParcelBoundary). In this example, ParcelBoundaries render as coloured boxes. As you type in an input, the colours will change to indicate which ParcelBoundaries have re-rendered. ```js import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; +import useParcelState from 'react-dataparcels/useParcelState'; import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; - -const PersonParcelHoc = ParcelHoc({ - name: "personParcel", - valueFromProps: (/* props */) => ({ - name: { - first: "Robert", - last: "Clamps" - }, - age: "33", - height: "160" - }) -}); +import exampleFrame from 'component/exampleFrame'; const DebugRender = ({children}) => { // each render, have a new, random background colour @@ -527,8 +469,19 @@ const DebugRender = ({children}) => { return
{children}
; }; -const PersonEditor = (props) => { - let {personParcel} = props; +export default function PersonEditor(props) { + + let [personParcel] = useParcelState({ + value: { + name: { + first: "Robert", + last: "Clamps" + }, + age: "33", + height: "160" + } + }); + return
@@ -557,8 +510,6 @@ const PersonEditor = (props) => { }
; -}; - -export default PersonParcelHoc(PersonEditor); +} ``` diff --git a/packages/dataparcels-docs/src/sandbox/SandboxFrame.jsx b/packages/dataparcels-docs/src/sandbox/SandboxFrame.jsx deleted file mode 100644 index 9ccbd36a..00000000 --- a/packages/dataparcels-docs/src/sandbox/SandboxFrame.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import ParcelHoc from 'react-dataparcels/ParcelHoc'; -import ParcelBoundary from 'react-dataparcels/ParcelBoundary'; -import ExampleHoc from 'component/ExampleHoc'; - -const ExampleParcelHoc = ParcelHoc({ - name: "exampleParcel", - valueFromProps: (/* props */) => ({ - abc: 123, - def: 123 - }) -}); - -const ExampleEditor = ({exampleParcel}) => { - return
-

Frames: {exampleParcel._frame}

- - {(parcel, {release}) =>
-

Frames: {parcel._frame}

- - -
} -
- - - {(parcel) => } - -
; -}; - -export default ExampleParcelHoc(ExampleHoc(ExampleEditor)); diff --git a/packages/dataparcels-docs/yalc.lock b/packages/dataparcels-docs/yalc.lock index fb47cf2e..69ec359b 100644 --- a/packages/dataparcels-docs/yalc.lock +++ b/packages/dataparcels-docs/yalc.lock @@ -2,7 +2,7 @@ "version": "v1", "packages": { "react-dataparcels": { - "signature": "b5ed56cf6267d2b27b212aaee6a07878", + "signature": "4916a609eada24a6bc51d4da7b619360", "file": true, "replaced": "^0.19.0" }, diff --git a/packages/dataparcels-docs/yarn.lock b/packages/dataparcels-docs/yarn.lock index 9a5ad759..192e18e8 100644 --- a/packages/dataparcels-docs/yarn.lock +++ b/packages/dataparcels-docs/yarn.lock @@ -7101,11 +7101,12 @@ react-cool-storage@^0.1.1: react-sortable-hoc "1.4.0" "react-dataparcels@file:.yalc/react-dataparcels": - version "0.19.1-58faeabe" + version "0.19.1-71b80bcf" dependencies: "@babel/runtime" "^7.1.5" dataparcels "^0.19.1" unmutable "^0.41.1" + use-debounce "^1.1.3" react-dev-utils@^4.2.1: version "4.2.3" @@ -7143,10 +7144,6 @@ react-error-overlay@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" -react-flip-move@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/react-flip-move/-/react-flip-move-3.0.3.tgz#3065b0b9e622ae73953aba725f8feed8e4643667" - react-goose@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/react-goose/-/react-goose-0.6.2.tgz#8267af61aa1e7aa86a8d5f280a1acf880e16743b" @@ -8766,6 +8763,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-debounce@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-1.1.3.tgz#08b76fd8cc9b107e0a6d6ee8bb6c5fe97a14732f" + integrity sha512-pv/8dgE66/Qfp3eltmChJl3qdTZ+aOUqDSHN+vQQgujMa5gfG8GzPATxS5jEUwv7ajv6iFXrfcSpVykmkAgIAw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" diff --git a/packages/dataparcels/README.md b/packages/dataparcels/README.md index c75cf74b..f24dd849 100644 --- a/packages/dataparcels/README.md +++ b/packages/dataparcels/README.md @@ -11,4 +11,4 @@ A library for editing data structures that works really well with React. ## Packages -If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get components and hocs to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). +If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get hooks, hocs and components to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). diff --git a/packages/dataparcels/package.json b/packages/dataparcels/package.json index ebf6e26e..2ae09aed 100644 --- a/packages/dataparcels/package.json +++ b/packages/dataparcels/package.json @@ -25,7 +25,8 @@ }, "private": false, "scripts": { - "build": "rm -rf lib && NODE_ENV=production babel src --out-dir lib --ignore '**/__test__/*.js'", + "yalc-publish": "../../node_modules/.bin/yalc publish --push --force", + "build": "rm -rf lib && NODE_ENV=production babel src --out-dir lib --ignore '**/__test__/*.js && yarn yalc-publish'", "build-all": "yarn build", "flow": "blueflag-test flow", "flow-coverage": "blueflag-test flow-coverage", diff --git a/packages/dataparcels/src/index.js b/packages/dataparcels/src/index.js index bd580a80..d40830f9 100644 --- a/packages/dataparcels/src/index.js +++ b/packages/dataparcels/src/index.js @@ -25,4 +25,4 @@ export type {Index} from './types/Types'; export type {Property} from './types/Types'; export type {ParentType} from './types/Types'; -export type {ContinueChainFunction} from './types/Types'; +export type {ParcelPlugin} from './types/Types'; diff --git a/packages/dataparcels/src/types/Types.js b/packages/dataparcels/src/types/Types.js index e98a816e..db49899a 100644 --- a/packages/dataparcels/src/types/Types.js +++ b/packages/dataparcels/src/types/Types.js @@ -77,7 +77,9 @@ export type ParcelIdData = { path: string[] }; -export type ContinueChainFunction = (continueChain: () => void, changeRequest: ?ChangeRequest) => void; +export type ParcelPlugin = { + beforeChange?: ParcelValueUpdater +}; const RUNTIME_TYPES = { ['boolean']: { diff --git a/packages/dataparcels/src/validation/Validation.js b/packages/dataparcels/src/validation/Validation.js index fc55dbba..54aa9ce2 100644 --- a/packages/dataparcels/src/validation/Validation.js +++ b/packages/dataparcels/src/validation/Validation.js @@ -1,6 +1,5 @@ // @flow -import type {ContinueChainFunction} from '../types/Types'; import type {ParcelDataEvaluator} from '../types/Types'; import type {ParcelValueUpdater} from '../types/Types'; @@ -21,12 +20,11 @@ type ValidationRuleMap = { }; type ValidationResult = { - modifyBeforeUpdate: ParcelValueUpdater, - onRelease: ContinueChainFunction + beforeChange: ParcelValueUpdater }; export default (validatorMap: ValidationRuleMap): ValidationResult => { - let modifyBeforeUpdate = dangerouslyUpdateParcelData((parcelData) => { + let beforeChange = dangerouslyUpdateParcelData((parcelData) => { let allValid = true; let mapValidationRuleApplier = (validator: ValidationRule|ValidationRule[], path: string): ParcelDataEvaluator => { @@ -69,15 +67,7 @@ export default (validatorMap: ValidationRuleMap): ValidationResult => { ); }); - // $FlowFixMe - let shouldSubmit = (parcelData) => !!(parcelData.meta.valid); - - let onRelease = (continueRelease, changeRequest) => { - changeRequest && shouldSubmit(changeRequest.nextData) && continueRelease(); - }; - return { - modifyBeforeUpdate, - onRelease + beforeChange }; }; diff --git a/packages/dataparcels/src/validation/__test__/Validation-test.js b/packages/dataparcels/src/validation/__test__/Validation-test.js index 51fa73da..c8049932 100644 --- a/packages/dataparcels/src/validation/__test__/Validation-test.js +++ b/packages/dataparcels/src/validation/__test__/Validation-test.js @@ -3,8 +3,8 @@ import Validation from '../Validation'; // _dangerouslyUpdate -test('Validation should use a dangerous updater, so it will work in modifyBeforeUpdate', () => { - expect(Validation({}).modifyBeforeUpdate._dangerouslyUpdate).toBe(true); +test('Validation should use a dangerous updater, so it will work in beforeChange', () => { + expect(Validation({}).beforeChange._dangerouslyUpdate).toBe(true); }); @@ -23,7 +23,7 @@ test('Validation should validate specified fields', () => { def: isValid }); - let newParcelData = validation.modifyBeforeUpdate(parcelData); + let newParcelData = validation.beforeChange(parcelData); // validator should be called expect(isValid.mock.calls[0][0]).toBe(123); @@ -54,7 +54,7 @@ test('Validation should accept arrays of validators', () => { ghi: [higherThan300, higherThan600] }); - let newParcelData = validation.modifyBeforeUpdate(parcelData); + let newParcelData = validation.beforeChange(parcelData); // meta should be set expect(newParcelData.child.abc.meta.invalid).toBe("Not higher than 300"); @@ -75,7 +75,7 @@ test('Validation should accept wildcards to check all fields at a depth', () => ['abc.*']: value => value > 4 ? "Too big" : undefined }); - let newParcelData = validation.modifyBeforeUpdate(parcelData); + let newParcelData = validation.beforeChange(parcelData); // meta should be set expect(newParcelData.child.abc.child[0].meta.invalid).toBe(undefined); @@ -98,7 +98,7 @@ test('Validation should accept multiple wildcards to check all fields at a depth ['*.*']: value => value > 4 ? "Too big" : undefined }); - let newParcelData = validation.modifyBeforeUpdate(parcelData); + let newParcelData = validation.beforeChange(parcelData); // meta should be set expect(newParcelData.child.abc.child[0].meta.invalid).toBe(undefined); @@ -127,43 +127,11 @@ test('Validation should set top level meta.valid', () => { abc: value => value > 300 ? undefined : "Not higher than 300" }); - let newParcelData = validation.modifyBeforeUpdate(parcelData); - let newParcelData2 = validation.modifyBeforeUpdate(parcelData2); + let newParcelData = validation.beforeChange(parcelData); + let newParcelData2 = validation.beforeChange(parcelData2); // meta should be set expect(newParcelData.meta.valid).toBe(false); expect(newParcelData2.meta.valid).toBe(true); }); -test('Validation onRelease should continue chain if meta.valid is true', () => { - - let continueRelease = jest.fn(); - let changeRequestValid = { - nextData: { - meta: { - valid: true - } - } - }; - - // $FlowFixMe - Validation({}).onRelease(continueRelease, changeRequestValid); - expect(continueRelease).toHaveBeenCalled(); -}); - -test('Validation onRelease should not continue chain if meta.valid is false', () => { - - let continueRelease = jest.fn(); - let changeRequestValid = { - nextData: { - meta: { - valid: false - } - } - }; - - // $FlowFixMe - Validation({}).onRelease(continueRelease, changeRequestValid); - expect(continueRelease).not.toHaveBeenCalled(); -}); - diff --git a/packages/react-dataparcels/ParcelBufferControl.js b/packages/react-dataparcels/ParcelBufferControl.js new file mode 100644 index 00000000..d7ba84bd --- /dev/null +++ b/packages/react-dataparcels/ParcelBufferControl.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +module.exports = require('./lib/ParcelBufferControl.js'); diff --git a/packages/react-dataparcels/README.md b/packages/react-dataparcels/README.md index c75cf74b..f24dd849 100644 --- a/packages/react-dataparcels/README.md +++ b/packages/react-dataparcels/README.md @@ -11,4 +11,4 @@ A library for editing data structures that works really well with React. ## Packages -If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get components and hocs to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). +If you're using React, get [`react-dataparcels`](https://www.npmjs.com/package/react-dataparcels) and you'll also get hooks, hocs and components to easily use dataparcels in a React app. If not, go for [`dataparcels`](https://www.npmjs.com/package/dataparcels). diff --git a/packages/react-dataparcels/__test__/Exports-test.js b/packages/react-dataparcels/__test__/Exports-test.js index e9045c04..14f7c8cd 100644 --- a/packages/react-dataparcels/__test__/Exports-test.js +++ b/packages/react-dataparcels/__test__/Exports-test.js @@ -14,6 +14,9 @@ import Validation from '../Validation'; import ParcelHoc from '../ParcelHoc'; import ParcelBoundary from '../ParcelBoundary'; import ParcelBoundaryHoc from '../ParcelBoundaryHoc'; +import ParcelBufferControl from '../ParcelBufferControl'; +import useParcelBuffer from '../useParcelBuffer'; +import useParcelState from '../useParcelState'; // internal dataparcels files import InternalParcel from 'dataparcels'; @@ -29,6 +32,9 @@ import InternalValidation from 'dataparcels/Validation'; import InternalParcelHoc from '../lib/ParcelHoc'; import InternalParcelBoundary from '../lib/ParcelBoundary'; import InternalParcelBoundaryHoc from '../lib/ParcelBoundaryHoc'; +import InternalParcelBufferControl from '../lib/ParcelBufferControl'; +import InternalUseParcelBuffer from '../lib/useParcelBuffer'; +import InternalUseParcelState from '../lib/useParcelState'; test('index should export Parcel', () => { expect(Parcel).toBe(InternalParcel); @@ -73,3 +79,15 @@ test('/ParcelBoundary should export ParcelBoundary', () => { test('/ParcelBoundaryHoc should export ParcelBoundaryHoc', () => { expect(ParcelBoundaryHoc).toBe(InternalParcelBoundaryHoc); }); + +test('/ParcelBufferControl should export ParcelBufferControl', () => { + expect(ParcelBufferControl).toBe(InternalParcelBufferControl); +}); + +test('/useParcelBuffer should export useParcelBuffer', () => { + expect(useParcelBuffer).toBe(InternalUseParcelBuffer); +}); + +test('/useParcelState should export useParcelState', () => { + expect(useParcelState).toBe(InternalUseParcelState); +}); diff --git a/packages/react-dataparcels/jest.config.js b/packages/react-dataparcels/jest.config.js index 919480f7..c83979d2 100644 --- a/packages/react-dataparcels/jest.config.js +++ b/packages/react-dataparcels/jest.config.js @@ -9,7 +9,7 @@ module.exports = { coverageThreshold: { global: { statements: 100, - branches: 99, + branches: 100, functions: 100, lines: 100 } diff --git a/packages/react-dataparcels/package.json b/packages/react-dataparcels/package.json index 75a4a693..1d6bf553 100644 --- a/packages/react-dataparcels/package.json +++ b/packages/react-dataparcels/package.json @@ -17,9 +17,12 @@ "DeletedParcelMarker.js", "ParcelBoundary.js", "ParcelBoundaryHoc.js", + "ParcelBufferControl.js", "ParcelHoc.js", "ParcelShape.js", "shape.js", + "useParcelBuffer.js", + "useParcelState.js", "Validation.js" ], "bugs": { @@ -41,7 +44,8 @@ "dependencies": { "@babel/runtime": "^7.1.5", "dataparcels": "^0.19.1", - "unmutable": "^0.41.1" + "unmutable": "^0.41.1", + "use-debounce": "^1.1.3" }, "devDependencies": { "@babel/cli": "^7.1.2", @@ -51,6 +55,7 @@ "immutable": "^4.0.0-rc.12", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-hooks-testing-library": "^0.5.0", "size-limit": "^0.21.1" }, "peerDependencies": { diff --git a/packages/react-dataparcels/src/ParcelBoundary.jsx b/packages/react-dataparcels/src/ParcelBoundary.jsx index fbb9fbc7..8ce7bc20 100644 --- a/packages/react-dataparcels/src/ParcelBoundary.jsx +++ b/packages/react-dataparcels/src/ParcelBoundary.jsx @@ -1,338 +1,60 @@ // @flow import type {Node} from 'react'; -import type ChangeRequest from 'dataparcels/ChangeRequest'; -import type {ContinueChainFunction} from 'dataparcels'; -import type {ParcelData} from 'dataparcels'; +import type Parcel from 'dataparcels'; +import type ParcelBufferControl from './ParcelBufferControl'; import type {ParcelValueUpdater} from 'dataparcels'; -import React from 'react'; -import Parcel from 'dataparcels'; -import dangerouslyUpdateParcelData from 'dataparcels/dangerouslyUpdateParcelData'; +// $FlowFixMe - useMemo is a named export of react +import {useMemo} from 'react'; +import useParcelBuffer from './useParcelBuffer'; -import ParcelBoundaryControl from './ParcelBoundaryControl'; -import ApplyModifyBeforeUpdate from './util/ApplyModifyBeforeUpdate'; -import ParcelBoundaryEquals from './util/ParcelBoundaryEquals'; - -import identity from 'unmutable/identity'; -import isNotEmpty from 'unmutable/isNotEmpty'; -import pipe from 'unmutable/pipe'; -import set from 'unmutable/set'; -import shallowEquals from 'unmutable/shallowEquals'; - -type RenderFunction = (parcel: Parcel, control: ParcelBoundaryControl) => Node; +type RenderFunction = (parcel: Parcel, buffer: ParcelBufferControl) => Node; type Props = { - children: RenderFunction, - debounce: number, - debugBuffer: boolean, - debugParcel: boolean, - hold: boolean, - forceUpdate: Array<*>, - modifyBeforeUpdate: Array, - onCancel: Array, - onRelease: Array, - parcel: Parcel, - pure: boolean, - keepValue: boolean -}; - -type State = { - cachedChangeRequest: ?ChangeRequest, - changeCount: number, - lastValueFromSelf: any, - makeBoundarySplit: Function, parcel: Parcel, - parcelFromProps: Parcel + children: RenderFunction, + pure?: boolean, + forceUpdate?: any[], + debounce?: number, + hold?: boolean, + beforeChange?: ParcelValueUpdater|ParcelValueUpdater[], + modifyBeforeUpdate?: ParcelValueUpdater|ParcelValueUpdater[], + keepValue?: boolean }; -const valueEquals = (a, b): boolean => a === b || (a !== a && b !== b); - -export default class ParcelBoundary extends React.Component { /* eslint-disable-line react/no-deprecated */ - - static defaultProps: * = { - debounce: 0, - debugBuffer: false, - debugParcel: false, - hold: false, - forceUpdate: [], - modifyBeforeUpdate: [], - onCancel: [], - onRelease: [], - pure: true, - keepValue: false - }; - - constructor(props: Props) { - super(props); - - let parcel = this.makeBoundarySplit(props.parcel); - - this.state = { - cachedChangeRequest: undefined, - changeCount: 0, - lastValueFromSelf: parcel.value, - makeBoundarySplit: this.makeBoundarySplit, - parcel, - parcelFromProps: parcel - }; - - if(process.env.NODE_ENV !== 'production' && props.debugParcel) { - console.log(`ParcelBoundary: Received initial value:`); // eslint-disable-line - console.log(props.parcel.data); // eslint-disable-line - } +export default function ParcelBoundary(props: Props): Node { + let { + parcel, + children, + pure = true, + forceUpdate = [], + debounce = 0, + hold = false, + beforeChange = [], + modifyBeforeUpdate = [], + keepValue = false + } = props; + + // deprecation notice + if(process.env.NODE_ENV !== 'production' && modifyBeforeUpdate.length > 0) { + console.warn(`ParcelBoundary.modifyBeforeUpdate is deprecated. Please use ParcelBoundary.beforeChange instead`); + beforeChange = beforeChange.concat(modifyBeforeUpdate); } - shouldComponentUpdate(nextProps: Props, nextState: State): boolean { - if(!nextProps.pure) { - return true; - } - - let parcelDataChanged: boolean = !ParcelBoundaryEquals(this.props.parcel, nextProps.parcel); - - if(!parcelDataChanged) { - parcelDataChanged = !ParcelBoundaryEquals(this.state.parcel, nextState.parcel); - } - - let forceUpdateChanged: boolean = !shallowEquals(this.props.forceUpdate)(nextProps.forceUpdate); - let cachedChangeRequestChanged: boolean = this.state.cachedChangeRequest !== nextState.cachedChangeRequest; - - return parcelDataChanged || forceUpdateChanged || cachedChangeRequestChanged; - } - - static getDerivedStateFromProps(props: Props, state: State): * { - let { - parcel, - keepValue - } = props; - - let { - lastValueFromSelf, - makeBoundarySplit, - parcelFromProps, - parcel: parcelFromState - } = state; - - let newState = {}; - - let newParcelFromProps = parcel !== parcelFromProps; - if(newParcelFromProps) { - newState.parcelFromProps = parcel; - } - - if(newParcelFromProps && !ParcelBoundaryEquals(parcelFromProps, parcel)) { - let newData = parcel.data; - - if(keepValue) { - let changedBySelf = parcel._lastOriginId.startsWith(parcel.id); - if(changedBySelf) { - newState.lastValueFromSelf = parcel.value; - } - - if(changedBySelf || valueEquals(newData.value, lastValueFromSelf)) { - newData = { - ...parcelFromState.data, - key: newData.key, - meta: newData.meta - }; - } - } - - if(process.env.NODE_ENV !== 'production' && props.debugParcel) { - console.log(`ParcelBoundary: Parcel replaced from props:`); // eslint-disable-line - console.log(newData); // eslint-disable-line - } - - newState.cachedChangeRequest = undefined; - newState.changeCount = 0; - newState.parcel = makeBoundarySplit(parcel, newData, parcelFromState.data); - } - - return isNotEmpty()(newState) ? newState : null; - } + let [innerParcel, parcelBufferControl] = useParcelBuffer({ + parcel, + debounce, + hold, + beforeChange, + keepValue + }); - addToBuffer: Function = (changeRequest: ChangeRequest) => (state: State): State => { - let {debugBuffer} = this.props; - let { - cachedChangeRequest, - changeCount - } = state; + let renderChildren = () => children(innerParcel, parcelBufferControl); - if(process.env.NODE_ENV !== 'production' && debugBuffer) { - console.log(`ParcelBoundary: Add to buffer:`); // eslint-disable-line - console.log(changeRequest.toJS()); // eslint-disable-line - } + let memoed = useMemo( + () => pure && renderChildren(), + [innerParcel, ...forceUpdate] + ); - let newCachedChangeRequest = cachedChangeRequest - ? cachedChangeRequest.merge(changeRequest) - : changeRequest; - - return { - ...state, - cachedChangeRequest: newCachedChangeRequest, - changeCount: changeCount + 1 - }; - }; - - cancelBuffer: Function = () => (state: State): State => { - let { - debugBuffer, - debugParcel, - parcel - } = this.props; - - let {cachedChangeRequest} = state; - - if(process.env.NODE_ENV !== 'production' && debugBuffer) { - console.log(`ParcelBoundary: Clear buffer:`); // eslint-disable-line - cachedChangeRequest && console.log(cachedChangeRequest.toJS()); // eslint-disable-line - } - if(!cachedChangeRequest) { - return state; - } - - if(process.env.NODE_ENV !== 'production' && debugParcel) { - console.log(`ParcelBoundary: Buffer cancelled. Parcel reverted:`); // eslint-disable-line - console.log(parcel.data); // eslint-disable-line - } - - return { - ...state, - cachedChangeRequest: undefined, - changeCount: 0, - parcel: this.makeBoundarySplit(parcel) - }; - }; - - releaseBuffer: Function = () => (state: State): State => { - let {debugBuffer} = this.props; - let {cachedChangeRequest} = state; - - if(process.env.NODE_ENV !== 'production' && debugBuffer) { - console.log(`ParcelBoundary: Release buffer:`); // eslint-disable-line - cachedChangeRequest && console.log(cachedChangeRequest.toJS()); // eslint-disable-line - } - - if(!cachedChangeRequest) { - return state; - } - - this.props.parcel.dispatch(cachedChangeRequest); - return { - ...state, - cachedChangeRequest: undefined, - changeCount: 0 - }; - }; - - makeBoundarySplit: Function = (parcel: Parcel, nextData: ?ParcelData, prevData: ?ParcelData): Parcel => { - let {modifyBeforeUpdate} = this.props; - - let handleChange = (newParcel: Parcel, changeRequest: ChangeRequest) => { - let { - debounce, - debugParcel, - hold, - keepValue - } = this.props; - - let {changeCount} = this.state; - - if(process.env.NODE_ENV !== 'production' && debugParcel) { - console.log(`ParcelBoundary: Parcel changed:`); // eslint-disable-line - console.log(newParcel.data); // eslint-disable-line - } - - let updateParcel = set('parcel', newParcel); - let addToBuffer = this.addToBuffer(changeRequest); - let releaseBuffer = this.releaseBuffer(); - - if(!debounce && !hold) { - this.setState(pipe( - keepValue ? updateParcel : ii => ii, - addToBuffer, - releaseBuffer - )); - return; - } - - if(hold) { - this.setState(pipe( - updateParcel, - addToBuffer - )); - return; - } - - // debounce && !hold - - setTimeout(() => { - if(changeCount + 1 === this.state.changeCount) { - this.setState(releaseBuffer); - } - }, debounce); - - this.setState(pipe( - updateParcel, - addToBuffer - )); - }; - - return parcel - ._boundarySplit({ - handleChange - }) - ._changeAndReturn((parcel) => parcel - .modifyDown(prevData - ? dangerouslyUpdateParcelData(() => prevData) - : identity() - ) - .pipe(ApplyModifyBeforeUpdate(modifyBeforeUpdate)) - ._setData(nextData || parcel.data) - ); - }; - - render(): Node { - let { - children, - modifyBeforeUpdate, - onCancel, - onRelease - } = this.props; - - let { - cachedChangeRequest, - parcel - } = this.state; - - let actions = cachedChangeRequest - ? cachedChangeRequest.actions - : []; - - let handleCancel = () => this.setState(this.cancelBuffer()); - let handleRelease = () => this.setState(this.releaseBuffer()); - - let chain = (callbackArray, finalCallback) => callbackArray.reduceRight( - (continueChain, callback) => () => { - let {cachedChangeRequest, parcel} = this.state; - return callback( - continueChain, - cachedChangeRequest && cachedChangeRequest._create({ - prevData: parcel.data - }) - ); - }, - finalCallback - ); - - return children( - ApplyModifyBeforeUpdate(modifyBeforeUpdate)(parcel), - new ParcelBoundaryControl({ - cancel: chain(onCancel, handleCancel), - release: chain(onRelease, handleRelease), - buffered: actions.length > 0, - buffer: actions, - originalParcel: this.props.parcel - }) - ); - } + return pure ? memoed : renderChildren(); } diff --git a/packages/react-dataparcels/src/ParcelBoundaryControl.js b/packages/react-dataparcels/src/ParcelBoundaryControlDeprecated.jsx similarity index 100% rename from packages/react-dataparcels/src/ParcelBoundaryControl.js rename to packages/react-dataparcels/src/ParcelBoundaryControlDeprecated.jsx diff --git a/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx b/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx new file mode 100644 index 00000000..0e1d1c01 --- /dev/null +++ b/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx @@ -0,0 +1,342 @@ +// @flow +import type {Node} from 'react'; +import type ChangeRequest from 'dataparcels/ChangeRequest'; +import type {ContinueChainFunction} from 'dataparcels'; +import type {ParcelData} from 'dataparcels'; +import type {ParcelValueUpdater} from 'dataparcels'; + +import React from 'react'; +import Parcel from 'dataparcels'; +import dangerouslyUpdateParcelData from 'dataparcels/dangerouslyUpdateParcelData'; + +import ParcelBoundaryControl from './ParcelBoundaryControlDeprecated'; +import ParcelBoundaryEquals from './util/ParcelBoundaryEquals'; + +import identity from 'unmutable/identity'; +import isNotEmpty from 'unmutable/isNotEmpty'; +import pipe from 'unmutable/pipe'; +import set from 'unmutable/set'; +import shallowEquals from 'unmutable/shallowEquals'; +import compose from 'unmutable/compose'; + +const ApplyModifyBeforeUpdate = (modifyBeforeUpdate: Array) => compose( + ...modifyBeforeUpdate.map((fn) => parcel => parcel.modifyUp(fn)) +); + +type RenderFunction = (parcel: Parcel, control: ParcelBoundaryControl) => Node; + +type Props = { + children: RenderFunction, + debounce: number, + debugBuffer: boolean, + debugParcel: boolean, + hold: boolean, + forceUpdate: Array<*>, + modifyBeforeUpdate: Array, + onCancel: Array, + onRelease: Array, + parcel: Parcel, + pure: boolean, + keepValue: boolean +}; + +type State = { + cachedChangeRequest: ?ChangeRequest, + changeCount: number, + lastValueFromSelf: any, + makeBoundarySplit: Function, + parcel: Parcel, + parcelFromProps: Parcel +}; + +const valueEquals = (a, b): boolean => a === b || (a !== a && b !== b); + +export default class ParcelBoundary extends React.Component { /* eslint-disable-line react/no-deprecated */ + + static defaultProps: * = { + debounce: 0, + debugBuffer: false, + debugParcel: false, + hold: false, + forceUpdate: [], + modifyBeforeUpdate: [], + onCancel: [], + onRelease: [], + pure: true, + keepValue: false + }; + + constructor(props: Props) { + super(props); + + let parcel = this.makeBoundarySplit(props.parcel); + + this.state = { + cachedChangeRequest: undefined, + changeCount: 0, + lastValueFromSelf: parcel.value, + makeBoundarySplit: this.makeBoundarySplit, + parcel, + parcelFromProps: parcel + }; + + if(process.env.NODE_ENV !== 'production' && props.debugParcel) { + console.log(`ParcelBoundary: Received initial value:`); // eslint-disable-line + console.log(props.parcel.data); // eslint-disable-line + } + } + + shouldComponentUpdate(nextProps: Props, nextState: State): boolean { + if(!nextProps.pure) { + return true; + } + + let parcelDataChanged: boolean = !ParcelBoundaryEquals(this.props.parcel, nextProps.parcel); + + if(!parcelDataChanged) { + parcelDataChanged = !ParcelBoundaryEquals(this.state.parcel, nextState.parcel); + } + + let forceUpdateChanged: boolean = !shallowEquals(this.props.forceUpdate)(nextProps.forceUpdate); + let cachedChangeRequestChanged: boolean = this.state.cachedChangeRequest !== nextState.cachedChangeRequest; + + return parcelDataChanged || forceUpdateChanged || cachedChangeRequestChanged; + } + + static getDerivedStateFromProps(props: Props, state: State): * { + let { + parcel, + keepValue + } = props; + + let { + lastValueFromSelf, + makeBoundarySplit, + parcelFromProps, + parcel: parcelFromState + } = state; + + let newState = {}; + + let newParcelFromProps = parcel !== parcelFromProps; + if(newParcelFromProps) { + newState.parcelFromProps = parcel; + } + + if(newParcelFromProps && !ParcelBoundaryEquals(parcelFromProps, parcel)) { + let newData = parcel.data; + + if(keepValue) { + let changedBySelf = parcel._lastOriginId.startsWith(parcel.id); + if(changedBySelf) { + newState.lastValueFromSelf = parcel.value; + } + + if(changedBySelf || valueEquals(newData.value, lastValueFromSelf)) { + newData = { + ...parcelFromState.data, + key: newData.key, + meta: newData.meta + }; + } + } + + if(process.env.NODE_ENV !== 'production' && props.debugParcel) { + console.log(`ParcelBoundary: Parcel replaced from props:`); // eslint-disable-line + console.log(newData); // eslint-disable-line + } + + newState.cachedChangeRequest = undefined; + newState.changeCount = 0; + newState.parcel = makeBoundarySplit(parcel, newData, parcelFromState.data); + } + + return isNotEmpty()(newState) ? newState : null; + } + + addToBuffer: Function = (changeRequest: ChangeRequest) => (state: State): State => { + let {debugBuffer} = this.props; + let { + cachedChangeRequest, + changeCount + } = state; + + if(process.env.NODE_ENV !== 'production' && debugBuffer) { + console.log(`ParcelBoundary: Add to buffer:`); // eslint-disable-line + console.log(changeRequest.toJS()); // eslint-disable-line + } + + let newCachedChangeRequest = cachedChangeRequest + ? cachedChangeRequest.merge(changeRequest) + : changeRequest; + + return { + ...state, + cachedChangeRequest: newCachedChangeRequest, + changeCount: changeCount + 1 + }; + }; + + cancelBuffer: Function = () => (state: State): State => { + let { + debugBuffer, + debugParcel, + parcel + } = this.props; + + let {cachedChangeRequest} = state; + + if(process.env.NODE_ENV !== 'production' && debugBuffer) { + console.log(`ParcelBoundary: Clear buffer:`); // eslint-disable-line + cachedChangeRequest && console.log(cachedChangeRequest.toJS()); // eslint-disable-line + } + if(!cachedChangeRequest) { + return state; + } + + if(process.env.NODE_ENV !== 'production' && debugParcel) { + console.log(`ParcelBoundary: Buffer cancelled. Parcel reverted:`); // eslint-disable-line + console.log(parcel.data); // eslint-disable-line + } + + return { + ...state, + cachedChangeRequest: undefined, + changeCount: 0, + parcel: this.makeBoundarySplit(parcel) + }; + }; + + releaseBuffer: Function = () => (state: State): State => { + let {debugBuffer} = this.props; + let {cachedChangeRequest} = state; + + if(process.env.NODE_ENV !== 'production' && debugBuffer) { + console.log(`ParcelBoundary: Release buffer:`); // eslint-disable-line + cachedChangeRequest && console.log(cachedChangeRequest.toJS()); // eslint-disable-line + } + + if(!cachedChangeRequest) { + return state; + } + + this.props.parcel.dispatch(cachedChangeRequest); + return { + ...state, + cachedChangeRequest: undefined, + changeCount: 0 + }; + }; + + makeBoundarySplit: Function = (parcel: Parcel, nextData: ?ParcelData, prevData: ?ParcelData): Parcel => { + let {modifyBeforeUpdate} = this.props; + + let handleChange = (newParcel: Parcel, changeRequest: ChangeRequest) => { + let { + debounce, + debugParcel, + hold, + keepValue + } = this.props; + + let {changeCount} = this.state; + + if(process.env.NODE_ENV !== 'production' && debugParcel) { + console.log(`ParcelBoundary: Parcel changed:`); // eslint-disable-line + console.log(newParcel.data); // eslint-disable-line + } + + let updateParcel = set('parcel', newParcel); + let addToBuffer = this.addToBuffer(changeRequest); + let releaseBuffer = this.releaseBuffer(); + + if(!debounce && !hold) { + this.setState(pipe( + keepValue ? updateParcel : ii => ii, + addToBuffer, + releaseBuffer + )); + return; + } + + if(hold) { + this.setState(pipe( + updateParcel, + addToBuffer + )); + return; + } + + // debounce && !hold + + setTimeout(() => { + if(changeCount + 1 === this.state.changeCount) { + this.setState(releaseBuffer); + } + }, debounce); + + this.setState(pipe( + updateParcel, + addToBuffer + )); + }; + + return parcel + ._boundarySplit({ + handleChange + }) + ._changeAndReturn((parcel) => parcel + .modifyDown(prevData + ? dangerouslyUpdateParcelData(() => prevData) + : identity() + ) + .pipe(ApplyModifyBeforeUpdate(modifyBeforeUpdate)) + ._setData(nextData || parcel.data) + ); + }; + + render(): Node { + let { + children, + modifyBeforeUpdate, + onCancel, + onRelease + } = this.props; + + let { + cachedChangeRequest, + parcel + } = this.state; + + let actions = cachedChangeRequest + ? cachedChangeRequest.actions + : []; + + let handleCancel = () => this.setState(this.cancelBuffer()); + let handleRelease = () => this.setState(this.releaseBuffer()); + + let chain = (callbackArray, finalCallback) => callbackArray.reduceRight( + (continueChain, callback) => () => { + let {cachedChangeRequest, parcel} = this.state; + return callback( + continueChain, + cachedChangeRequest && cachedChangeRequest._create({ + prevData: parcel.data + }) + ); + }, + finalCallback + ); + + return children( + ApplyModifyBeforeUpdate(modifyBeforeUpdate)(parcel), + new ParcelBoundaryControl({ + cancel: chain(onCancel, handleCancel), + release: chain(onRelease, handleRelease), + buffered: actions.length > 0, + buffer: actions, + originalParcel: this.props.parcel + }) + ); + } +} diff --git a/packages/react-dataparcels/src/ParcelBoundaryHoc.jsx b/packages/react-dataparcels/src/ParcelBoundaryHoc.jsx index ee5e8a7c..895acc28 100644 --- a/packages/react-dataparcels/src/ParcelBoundaryHoc.jsx +++ b/packages/react-dataparcels/src/ParcelBoundaryHoc.jsx @@ -4,10 +4,10 @@ import type {Node} from 'react'; import type Parcel from 'dataparcels'; import type {ContinueChainFunction} from 'dataparcels'; import type {ParcelValueUpdater} from 'dataparcels'; -import type ParcelBoundaryControl from './ParcelBoundaryControl'; +import type ParcelBoundaryControl from './ParcelBoundaryControlDeprecated'; import React from 'react'; -import ParcelBoundary from './ParcelBoundary'; +import ParcelBoundary from './ParcelBoundaryDeprecated'; import Types from 'dataparcels/lib/types/Types'; type Props = { @@ -39,6 +39,11 @@ const PARCEL_BOUNDARY_HOC_NAME = `ParcelBoundaryHoc()`; export default (config: ParcelBoundaryHocConfig): Function => { Types(`ParcelBoundaryHoc()`, `config`, `object`)(config); + // deprecation notice + if(process.env.NODE_ENV !== 'production') { + console.warn(`ParcelBoundaryHoc is deprecated. Please use the useParcelBuffer hook instead.`); + } + return (Component: ComponentType) => class ParcelBoundaryHoc extends React.Component { /* eslint-disable-line */ render(): Node { diff --git a/packages/react-dataparcels/src/ParcelBufferControl.js b/packages/react-dataparcels/src/ParcelBufferControl.js new file mode 100644 index 00000000..c80c8701 --- /dev/null +++ b/packages/react-dataparcels/src/ParcelBufferControl.js @@ -0,0 +1,26 @@ +// @flow +import type Action from 'dataparcels/Action'; + +type ParcelBufferControlConfig = { + submit: () => void, + reset: () => void, + buffered: boolean, + actions: Action[] +}; + +export default class ParcelBufferControl { + // actions + submit: () => void; + reset: () => void; + + // status + buffered: boolean; + actions: Action[]; + + constructor(config: ParcelBufferControlConfig) { + this.submit = config.submit; + this.reset = config.reset; + this.buffered = config.buffered; + this.actions = config.actions; + } +} diff --git a/packages/react-dataparcels/src/ParcelHoc.jsx b/packages/react-dataparcels/src/ParcelHoc.jsx index 604c47f2..5106d4f6 100644 --- a/packages/react-dataparcels/src/ParcelHoc.jsx +++ b/packages/react-dataparcels/src/ParcelHoc.jsx @@ -7,7 +7,7 @@ import type {ParcelValueUpdater} from 'dataparcels'; import React from 'react'; import Parcel from 'dataparcels'; import Types from 'dataparcels/lib/types/Types'; -import ApplyModifyBeforeUpdate from './util/ApplyModifyBeforeUpdate'; +import ApplyBeforeChange from './util/ApplyBeforeChange'; type Props = {}; @@ -45,6 +45,11 @@ const PARCEL_HOC_NAME = `ParcelHoc()`; export default (config: ParcelHocConfig): Function => { Types(`ParcelHoc()`, `config`, `object`)(config); + // deprecation notice + if(process.env.NODE_ENV !== 'production') { + console.warn(`ParcelHoc is deprecated. Please use the useParcelState hook instead.`); + } + let { name, valueFromProps, @@ -89,7 +94,7 @@ export default (config: ParcelHocConfig): Function => { static updateParcelValueFromProps(parcel: Parcel, props: Props): Parcel { return parcel._changeAndReturn((parcel: Parcel) => { let value: any = valueFromProps(props); - return ApplyModifyBeforeUpdate(modifyBeforeUpdate)(parcel).set(value); + return ApplyBeforeChange(modifyBeforeUpdate)(parcel).set(value); }); } diff --git a/packages/react-dataparcels/src/__test__/ParcelBoundary-test.js b/packages/react-dataparcels/src/__test__/ParcelBoundary-test.js index 58b86f5b..b9ab210a 100644 --- a/packages/react-dataparcels/src/__test__/ParcelBoundary-test.js +++ b/packages/react-dataparcels/src/__test__/ParcelBoundary-test.js @@ -1,87 +1,10 @@ // @flow import React from 'react'; import ParcelBoundary from '../ParcelBoundary'; -import ParcelBoundaryEquals from '../util/ParcelBoundaryEquals'; +import ParcelBufferControl from '../ParcelBufferControl'; import Parcel from 'dataparcels'; import Action from 'dataparcels/Action'; -jest.useFakeTimers(); - -test('ParcelBoundary should pass a *value equivalent* parcel to children', () => { - let parcel = new Parcel({ - value: 123 - }); - let childRenderer = jest.fn(); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - - expect(ParcelBoundaryEquals(childParcel, parcel)).toBe(true); -}); - -test('ParcelBoundary should send correct changes back up when debounce = 0', () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: 456, - handleChange - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - expect(handleChange).toHaveBeenCalledTimes(1); - let newParcel = handleChange.mock.calls[0][0]; - expect(newParcel.value).toBe(123); -}); - -test('ParcelBoundary should pass a NEW *value equivalent* parcel to children when props change', () => { - let childRenderer = jest.fn(); - - let parcel = new Parcel(); - let parcel2 = new Parcel({value: 456}); - - let wrapper = shallow( - {childRenderer} - ); - - wrapper.setProps({ - parcel: parcel2 - }); - - wrapper.update(); - - let childParcel = childRenderer.mock.calls[0][0]; - let childParcel2 = childRenderer.mock.calls[1][0]; - - expect(ParcelBoundaryEquals(childParcel, parcel)).toBe(true); - expect(ParcelBoundaryEquals(childParcel2, parcel2)).toBe(true); -}); - -test('ParcelBoundary should lock state to props if debounce, hold and keepValue are all false', () => { - let childRenderer = jest.fn(); - - let parcel = new Parcel({value: 123}); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.set(456); - - wrapper.update(); - - expect(childRenderer).toHaveBeenCalledTimes(1); -}); - test('ParcelBoundary should not rerender if parcel has not changed value and pure = true', () => { let childRenderer = jest.fn(); @@ -119,7 +42,8 @@ test('ParcelBoundary should rerender if parcel has not changed value and pure = wrapper.update(); - expect(childRenderer).toHaveBeenCalledTimes(2); + // greater than one because react with hooks can re-render more than just once + expect(childRenderer.mock.calls.length > 1).toBe(true); }); test('ParcelBoundary should rerender if parcel has not changed value but forceUpdate has', () => { @@ -143,184 +67,7 @@ test('ParcelBoundary should rerender if parcel has not changed value but forceUp expect(childRenderer).toHaveBeenCalledTimes(2); }); -test('ParcelBoundary should release changes when called', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - handleChange - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - // handleChange shouldn't be called yet because hold is true - expect(handleChange).toHaveBeenCalledTimes(0); - - wrapper.update(); - - let [childParcel2, control] = childRenderer.mock.calls[1]; - // inside the parcel boundary, the last change should be applied to the parcel - expect(childParcel2.value).toBe(123); - - control.release(); - - // handleChange should be called now because release() was called - expect(handleChange).toHaveBeenCalledTimes(1); - let newParcel = handleChange.mock.calls[0][0]; - - // handleChange should have been called with the correct value - expect(newParcel.value).toBe(123); -}); - -test('ParcelBoundary should use onRelease', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - let onRelease1 = jest.fn(); - let onRelease2 = jest.fn(); - - let parcel = new Parcel({ - handleChange - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - wrapper.update(); - - // call release, then onRelease1 should have been called but no others - childRenderer.mock.calls[1][1].release(); - expect(onRelease1).toHaveBeenCalledTimes(1); - expect(onRelease2).not.toHaveBeenCalled(); - expect(handleChange).not.toHaveBeenCalled(); - - // call release1, then onRelease2 should have been called - let release1 = onRelease1.mock.calls[0][0]; - release1(); - expect(onRelease1).toHaveBeenCalledTimes(1); - expect(onRelease2).toHaveBeenCalledTimes(1); - expect(handleChange).not.toHaveBeenCalled(); - - // call release2 and then handleChange should have been called - let release2 = onRelease2.mock.calls[0][0]; - release2(); - expect(onRelease1).toHaveBeenCalledTimes(1); - expect(onRelease2).toHaveBeenCalledTimes(1); - expect(handleChange).toHaveBeenCalledTimes(1); -}); - -test('ParcelBoundary should cancel changes when called', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - handleChange, - value: 456 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - // handleChange shouldn't be called yet because hold is true - expect(handleChange).toHaveBeenCalledTimes(0); - - wrapper.update(); - - let [childParcel2, control] = childRenderer.mock.calls[1]; - // inside the parcel boundary, the last change should be applied to the parcel - expect(childParcel2.value).toBe(123); - - control.cancel(); - - // handleChange should still not have been called - expect(handleChange).toHaveBeenCalledTimes(0); - - wrapper.update(); - - - let [childParcel3] = childRenderer.mock.calls[2]; - // inside the parcel boundary, the original value should be reinstated - expect(childParcel3.value).toBe(456); -}); - -test('ParcelBoundary should onCancel', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - let onCancel1 = jest.fn(); - let onCancel2 = jest.fn(); - - let parcel = new Parcel({ - handleChange, - value: 456 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - wrapper.update(); - let childRendererCalls = childRenderer.mock.calls.length; - - // call cancel, then onCancel1 should have been called but no others - childRenderer.mock.calls[1][1].cancel(); - expect(onCancel1).toHaveBeenCalledTimes(1); - expect(onCancel2).not.toHaveBeenCalled(); - // there should have been no re-render as the cancellation shouldn't have happened yet - expect(childRenderer).toHaveBeenCalledTimes(childRendererCalls); - - // call cancel1, then onCancel2 should have been called - let cancel1 = onCancel1.mock.calls[0][0]; - cancel1(); - expect(onCancel1).toHaveBeenCalledTimes(1); - expect(onCancel2).toHaveBeenCalledTimes(1); - // there should have been no re-render as the cancellation shouldn't have happened yet - expect(childRenderer).toHaveBeenCalledTimes(childRendererCalls); - - // call cancel and then inside the parcel boundary, the original value should be reinstated - let cancel2 = onCancel2.mock.calls[0][0]; - cancel2(); - wrapper.update(); - let [childParcel2] = childRenderer.mock.calls[2]; - // inside the parcel boundary, the original value should be reinstated - expect(childParcel2.value).toBe(456); -}); - - -test('ParcelBoundary cancel should do nothing if no changes have occurred', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - handleChange, - value: 456 - }); - - let wrapper = shallow( - {childRenderer} - ); - - childRenderer.mock.calls[0][1].cancel(); - - wrapper.update(); - - // childRenderer should still only have been called once - // because no change of state should have occurred whatsoever - expect(childRenderer).toHaveBeenCalledTimes(1); -}); - - -test('ParcelBoundary should pass buffer info to childRenderer', async () => { +test('ParcelBoundary should pass parcelBufferControl to childRenderer', async () => { let childRenderer = jest.fn(); let parcel = new Parcel(); @@ -332,480 +79,12 @@ test('ParcelBoundary should pass buffer info to childRenderer', async () => { let [childParcel, control] = childRenderer.mock.calls[0]; childParcel.onChange(123); // handleChange shouldn't be called yet because hold is true + expect(control instanceof ParcelBufferControl).toBe(true); expect(control.buffered).toBe(false); - expect(control.buffer.length).toBe(0); + expect(control.actions.length).toBe(0); let [childParcel2, control2] = childRenderer.mock.calls[1]; expect(control2.buffered).toBe(true); - expect(control2.buffer.length).toBe(1); - expect(control2.buffer[0] instanceof Action).toBe(true); - - control.release(); - - let [childParcel3, control3] = childRenderer.mock.calls[2]; - expect(control3.buffered).toBe(false); - expect(control3.buffer.length).toBe(0); -}); - -test('ParcelBoundary should debounce', async () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: {a:1, b:2}, - handleChange - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - - // make a change with a value - childParcel.get('a').onChange(123); - - // handleChange shouldn't be called yet - expect(handleChange).toHaveBeenCalledTimes(0); - - // wait 20ms - jest.advanceTimersByTime(20); - - // handleChange shouldn't be called yet - expect(handleChange).toHaveBeenCalledTimes(0); - - wrapper.update(); - let childParcel2 = childRenderer.mock.calls[1][0]; - - // parcel inside parcel boundary should have updated - expect(childParcel2.value).toEqual({a:123, b:2}); - - // make another change with a value - childParcel2.get('a').onChange(456); - - // wait another 20ms - jest.advanceTimersByTime(20); - - // handleChange still shouldn't be called yet - expect(handleChange).toHaveBeenCalledTimes(0); - - wrapper.update(); - let childParcel3 = childRenderer.mock.calls[2][0]; - - // parcel inside parcel boundary should have updated - expect(childParcel3.value).toEqual({a:456, b:2}); - - // make another 2 changes with a value - childParcel3.get('a').onChange(789); - childParcel3.get('b').onChange(789); - - // wait another 40ms - with an interval this big, debounce should have finally had time to kick in - jest.advanceTimersByTime(40); - - // handleChange should have been called - expect(handleChange).toHaveBeenCalledTimes(1); - // handleChange should have been called with the most recent set of changes - expect(handleChange.mock.calls[0][0].value).toEqual({a:789, b:789}); -}); - -test('ParcelBoundary should cancel unreleased changes when receiving a new parcel prop', () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: 123, - handleChange - }); - let parcel2 = new Parcel({ - value: 456, - handleChange - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(789); - - let childParcel2 = childRenderer.mock.calls[1][0]; - - // verify that the current value of the parcel has been updated - expect(childParcel2.value).toBe(789); - - wrapper.setProps({ - parcel: parcel2 - }); - - let childParcel3 = childRenderer.mock.calls[2][0]; - // the new value received via props should be passed down WITHOUT the previous 789 change applied - expect(childParcel3.value).toBe(456); - - let control = childRenderer.mock.calls[2][1]; - control.release(); - - // after release()ing the buffer, handleChange should not be called, because there should not be anything in the buffer - expect(handleChange).toHaveBeenCalledTimes(0); -}); - -test('ParcelBoundary should use an internal boundary split to stop parcel boundaries using the same parcel from sharing their parcel registries', () => { - let parcel = new Parcel({ - value: { - abc: 123, - def: 123 - } - }); - let childRendererA = jest.fn(); - let childRendererB = jest.fn(); - - let wrapper1 = shallow( - {childRendererA} - ); - - let wrapper2 = shallow( - {childRendererB} - ); - - let childParcelA = childRendererA.mock.calls[0][0]; - childParcelA.get('abc').onChange(456); - - let childParcelB = childRendererB.mock.calls[0][0]; - childParcelB.get('def').onChange(456); - - wrapper1.update(); - wrapper2.update(); - - let childParcelA2 = childRendererA.mock.calls[1][0]; - let childParcelB2 = childRendererB.mock.calls[1][0]; - - expect(childParcelA2.value).toEqual({abc: 456, def: 123}); - expect(childParcelB2.value).toEqual({abc: 123, def: 456}); -}); - -test('ParcelBoundary should not update value from props for updates caused by themselves if keepValue is true', () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: 123, - handleChange - }); - - let withModify = (parcel) => parcel.modifyUp(value => value + 1); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(456); - - let newParcel = handleChange.mock.calls[0][0]; - - // verify that the current value of the parcel has been updated - expect(newParcel.value).toBe(457); - - wrapper.setProps({ - parcel: withModify(newParcel) - }); - - // expect that the value in the parcelboundary has not changed - // because the last change was triggered by this boundary - let childParcel2 = childRenderer.mock.calls[2][0]; - expect(childParcel2.value).toBe(456); - - // make a change externally and ensure that the value in the boundary does update - newParcel.set(789); - let newParcel2 = handleChange.mock.calls[1][0]; - wrapper.setProps({ - parcel: withModify(newParcel2) - }); - - let childParcel3 = childRenderer.mock.calls[3][0]; - expect(childParcel3.value).toBe(789); -}); - -test('ParcelBoundary should update meta from props for updates caused by themselves if keepValue is true', () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: 123, - handleChange - }); - - let withModify = (parcel) => parcel.modifyUp(value => value + 1); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(456); - - let newParcel = handleChange.mock.calls[0][0]; - - // make a change that keepValue will prevent from altering its value - wrapper.setProps({ - parcel: withModify(newParcel) - }); - - // make a change to meta externally and ensure that the value in the boundary does not update - // but meta does - newParcel.setMeta({ - abc: 789 - }); - let newParcel2 = handleChange.mock.calls[1][0]; - wrapper.setProps({ - parcel: withModify(newParcel2) - }); - - let childParcel3 = childRenderer.mock.calls[3][0]; - expect(childParcel3.value).toBe(456); - expect(childParcel3.meta.abc).toBe(789); -}); -test('ParcelBoundary should pass initial value through modifyBeforeUpdate', () => { - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let modifyBeforeUpdate = [ - value => value + 1 - ]; - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - expect(childParcel.value).toBe(124); -}); - -test('ParcelBoundary should pass changes through modifyBeforeUpdate', () => { - let childRenderer = jest.fn(); - let handleChange = jest.fn(); - - let parcel = new Parcel({ - value: 456, - handleChange - }); - - let modifyBeforeUpdate = [ - value => value + 1, - value => value + 1 - ]; - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - expect(handleChange).toHaveBeenCalledTimes(1); - let newParcel = handleChange.mock.calls[0][0]; - expect(newParcel.value).toBe(125); -}); - -test('ParcelBoundary should pass new parcel from props change through modifyBeforeUpdate', () => { - let childRenderer = jest.fn(); - - let parcel = new Parcel({value: 123}); - let parcel2 = new Parcel({value: 456}); - - let modifyBeforeUpdate = [ - jest.fn(value => value + 1), - jest.fn(value => value + 1) - ]; - - let wrapper = shallow( - {childRenderer} - ); - - wrapper.setProps({ - parcel: parcel2 - }); - - wrapper.update(); - - let childParcel = childRenderer.mock.calls[0][0]; - let childParcel2 = childRenderer.mock.calls[1][0]; - - expect(modifyBeforeUpdate[0].mock.calls[2][0]).toBe(456); - expect(modifyBeforeUpdate[0].mock.calls[2][1].prevData.value).toBe(125); - expect(modifyBeforeUpdate[0].mock.calls[2][1].nextData.value).toBe(456); - - expect(modifyBeforeUpdate[1].mock.calls[2][0]).toBe(457); - expect(modifyBeforeUpdate[1].mock.calls[2][1].prevData.value).toBe(125); - expect(modifyBeforeUpdate[1].mock.calls[2][1].nextData.value).toBe(457); -}); - -test('ParcelBoundary should accept a debugParcel boolean and log about receiving initial value', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {() =>
} - ); - - expect(console.log.mock.calls[0][0]).toBe("ParcelBoundary: Received initial value:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - -test('ParcelBoundary should accept a debugParcel boolean and log about parcel changing', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - - expect(console.log.mock.calls[2][0]).toBe("ParcelBoundary: Parcel changed:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - -test('ParcelBoundary should accept a debugParcel boolean and log about replacing parcel from props', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - - let parcel = new Parcel({ - value: 123 - }); - - let parcel2 = new Parcel({ - value: 456 - }); - - let wrapper = shallow( - {() =>
} - ); - - wrapper.setProps({ - parcel: parcel2 - }); - - wrapper.update(); - - expect(console.log.mock.calls[2][0]).toBe("ParcelBoundary: Parcel replaced from props:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - -test('ParcelBoundary should accept a debugParcel boolean and log about cancelling and reverting parcel', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - - wrapper.update(); - - childRenderer.mock.calls[1][1].cancel(); - - expect(console.log.mock.calls[4][0]).toBe("ParcelBoundary: Buffer cancelled. Parcel reverted:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - -test('ParcelBoundary should accept a debugBuffer boolean and log about adding to buffer', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - - expect(console.log.mock.calls[0][0]).toBe("ParcelBoundary: Add to buffer:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - -test('ParcelBoundary should accept a debugBuffer boolean and log about releasing buffer', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - - wrapper.update(); - - childRenderer.mock.calls[1][1].release(); - - expect(console.log.mock.calls[2][0]).toBe("ParcelBoundary: Release buffer:"); - // $FlowFixMe - console.log = log; // eslint-disable-line -}); - - -test('ParcelBoundary should accept a debugBuffer boolean and log about cancelling buffer', () => { - let {log} = console; - // $FlowFixMe - console.log = jest.fn(); // eslint-disable-line - let childRenderer = jest.fn(); - - let parcel = new Parcel({ - value: 123 - }); - - let wrapper = shallow( - {childRenderer} - ); - - let childParcel = childRenderer.mock.calls[0][0]; - childParcel.onChange(123); - - wrapper.update(); - - childRenderer.mock.calls[1][1].cancel(); - - expect(console.log.mock.calls[2][0]).toBe("ParcelBoundary: Clear buffer:"); - // $FlowFixMe - console.log = log; // eslint-disable-line + expect(control2.actions.length).toBe(1); + expect(control2.actions[0] instanceof Action).toBe(true); }); diff --git a/packages/react-dataparcels/src/__test__/ParcelBoundaryHoc-test.js b/packages/react-dataparcels/src/__test__/ParcelBoundaryHoc-test.js index 3b058e45..91556431 100644 --- a/packages/react-dataparcels/src/__test__/ParcelBoundaryHoc-test.js +++ b/packages/react-dataparcels/src/__test__/ParcelBoundaryHoc-test.js @@ -3,7 +3,7 @@ import React from 'react'; import Parcel from 'dataparcels'; import ParcelBoundaryHoc from '../ParcelBoundaryHoc'; -import ParcelBoundaryControl from '../ParcelBoundaryControl'; +import ParcelBoundaryControl from '../ParcelBoundaryControlDeprecated'; let shallowRenderHoc = (props, hock) => { let Component = hock((props) =>
); diff --git a/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js b/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js new file mode 100644 index 00000000..4d8675cd --- /dev/null +++ b/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js @@ -0,0 +1,457 @@ +// @flow +import {act} from 'react-hooks-testing-library'; +import {renderHook} from 'react-hooks-testing-library'; +import useParcelBuffer from '../useParcelBuffer'; +import Parcel from 'dataparcels'; + +jest.useFakeTimers(); + +const renderHookWithProps = (initialProps, callback) => renderHook(callback, {initialProps}); + +describe('useParcelBuffer should use config.parcel', () => { + + it('should pass through a parcels data', () => { + let parcel = new Parcel(); + + let {result} = renderHook(() => useParcelBuffer({parcel})); + + expect(result.current[0].data).toEqual(parcel.data); + }); + + it('should pass through a parcel on first hook call', () => { + let parcel = new Parcel(); + let hookRenderer = jest.fn(() => useParcelBuffer({parcel})); + + renderHook(hookRenderer); + expect(hookRenderer.mock.results[0].value[0] instanceof Parcel).toBe(true); + }); + + it('should propagate parcel change immediately', () => { + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 123, + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({parcel})); + + act(() => { + result.current[0].set(456); + }); + + expect(handleChange.mock.calls[0][0].value).toBe(456); + }); + + it('should pass same inner parcel if outer parcel is the same', () => { + + let parcel = new Parcel({ + value: 123 + }); + + let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBuffer({parcel})); + + let firstResult = result.current[0]; + + act(() => { + rerender({ + parcel + }); + }); + + expect(result.current[0]).toBe(firstResult); + }); + + it('should pass new inner parcel if outer parcel is different', () => { + + let parcel = new Parcel({ + value: 123 + }); + + let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBuffer({parcel})); + + act(() => { + rerender({ + parcel: new Parcel({ + value: 456 + }) + }); + }); + + expect(result.current[0].value).toEqual(456); + }); + + it('should not use the same registry between outer and inner parcels', () => { + + // sharing a registry could feasibly enable unsubmitted changes in one buffer + // to be leaked outside of that buffer during a change + + let parcel = new Parcel(); + let {result} = renderHook(() => useParcelBuffer({parcel})); + + expect(result.current[0]._registry).not.toBe(parcel._registry); + }); + +}); + +describe('useParcelBuffer should pass ParcelBoundaryControl', () => { + + it('should pass ParcelBoundaryControl as second element in returned array', () => { + let parcel = new Parcel(); + + let {result} = renderHook(() => useParcelBuffer({parcel})); + + expect(typeof result.current[1].reset).toBe("function"); + expect(typeof result.current[1].submit).toBe("function"); + expect(result.current[1].buffered).toBe(false); + expect(result.current[1].actions).toEqual([]); + }); + +}); + +describe('useParcelBuffer should use config.hold', () => { + + it('should hold changes', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 123, + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + hold: true + })); + + act(() => { + result.current[0].set(456); + }); + + expect(handleChange).not.toHaveBeenCalled(); + }); + + it('should submit changes', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: [], + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + hold: true + })); + + expect(result.current[1].buffered).toBe(false); + expect(result.current[1].actions.length).toBe(0); + expect(handleChange).toHaveBeenCalledTimes(0); + + act(() => { + result.current[0].push("A"); + }); + + expect(result.current[1].buffered).toBe(true); + expect(result.current[1].actions.length).toBe(1); + expect(handleChange).toHaveBeenCalledTimes(0); + + act(() => { + result.current[0].push("B"); + }); + + expect(result.current[1].buffered).toBe(true); + expect(result.current[1].actions.length).toBe(2); + expect(handleChange).toHaveBeenCalledTimes(0); + + act(() => { + result.current[1].submit(); + }); + + expect(result.current[1].buffered).toBe(false); + expect(result.current[1].actions.length).toBe(0); + expect(handleChange).toHaveBeenCalledTimes(1); + expect(handleChange.mock.calls[0][0].value).toEqual(["A", "B"]); + }); + + it('should try to submit changes but do nothing if there are no changes', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: [], + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + hold: true + })); + + act(() => { + result.current[1].submit(); + }); + + expect(handleChange).not.toHaveBeenCalled(); + }); + + it('should reset changes', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: [], + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + hold: true + })); + + expect(result.current[0].value).toEqual([]); + expect(result.current[1].buffered).toBe(false); + expect(result.current[1].actions.length).toBe(0); + expect(handleChange).toHaveBeenCalledTimes(0); + act(() => { + result.current[0].push("A"); + }); + + expect(result.current[0].value).toEqual(["A"]); + expect(result.current[1].buffered).toBe(true); + expect(result.current[1].actions.length).toBe(1); + expect(handleChange).toHaveBeenCalledTimes(0); + + act(() => { + result.current[1].reset(); + }); + + expect(result.current[0].value).toEqual([]); + expect(result.current[1].buffered).toBe(false); + expect(result.current[1].actions.length).toBe(0); + expect(handleChange).toHaveBeenCalledTimes(0); + }); + +}); + +describe('useParcelBuffer should use config.debounce', () => { + + it('should propagate parcel change after debounce ms have elapsed between changes', () => { + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: [], + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + debounce: 20 + })); + + act(() => { + result.current[0].push("A"); + }); + + act(() => { + jest.advanceTimersByTime(10); + }); + + act(() => { + result.current[0].push("B"); + }); + + act(() => { + jest.advanceTimersByTime(40); + }); + + act(() => { + result.current[0].push("C"); + }); + + act(() => { + jest.advanceTimersByTime(40); + }); + + expect(handleChange.mock.calls[0][0].value).toEqual(["A", "B"]); + expect(handleChange.mock.calls[1][0].value).toEqual(["A", "B", "C"]); + }); + + it('should flush debounced changes if config.debounce is removed', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 123, + handleChange + }); + + let {result, rerender} = renderHookWithProps({debounce: 200}, ({debounce}) => useParcelBuffer({ + parcel, + debounce + })); + + act(() => { + result.current[0].set(456); + }); + + act(() => { + jest.advanceTimersByTime(10); + }); + + expect(handleChange).not.toHaveBeenCalled(); + + act(() => { + rerender({ + debounce: 0 + }); + }); + + expect(handleChange.mock.calls[0][0].value).toBe(456); + }); +}); + +describe('useParcelBuffer should use config.beforeChange', () => { + + it('should apply single beforeChange to outer parcel', () => { + + let parcel = new Parcel({ + value: 123 + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + beforeChange: value => value * 2 + })); + + expect(result.current[0].value).toBe(246); + }); + + it('should apply multiple beforeChange to outer parcel', () => { + let parcel = new Parcel({ + value: 123 + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + beforeChange: [ + value => value * 2, + value => value + 5 + ] + })); + + expect(result.current[0].value).toBe(251); + }); + + it('should apply single beforeChange to inner parcel', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 0, + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + beforeChange: value => value * 2 + })); + + act(() => { + result.current[0].set(123); + }); + + expect(handleChange.mock.calls[0][0].value).toBe(246); + }); + + it('should apply multiple beforeChange to inner parcel', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 0, + handleChange + }); + + let {result} = renderHook(() => useParcelBuffer({ + parcel, + beforeChange: [ + value => value * 2, + value => value + 5 + ] + })); + + act(() => { + result.current[0].set(123); + }); + + expect(handleChange.mock.calls[0][0].value).toBe(251); + }); + + it('should apply single beforeChange to outer parcel after an update', () => { + + let parcel = new Parcel({ + value: 123 + }); + + let {result, rerender} = renderHook(() => useParcelBuffer({ + parcel, + beforeChange: value => value * 2, + hold: true + })); + + act(() => { + result.current[0].set(100); + rerender(); + }); + + expect(result.current[0].value).toBe(200); + }); + +}); + +describe('useParcelBuffer should use config.keepValue', () => { + + it('should keep value if change originated from self and different value is passed down', () => { + + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: { + abc: 100 + }, + handleChange + }); + + let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBuffer({ + keepValue: true, + parcel: parcel + .get('abc') + .modifyDown(value => `${value}`) + .modifyUp(value => Number(value)) + })); + + expect(result.current[0].value).toBe("100"); + + act(() => { + result.current[0].set("100!"); + }); + + let newParcel = handleChange.mock.calls[0][0]; + + expect(newParcel.value).toEqual({ + abc: NaN + }); + + act(() => { + rerender({ + parcel: newParcel + }); + }); + + expect(result.current[0].value).toBe("100!"); + }); + +}); diff --git a/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js b/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js new file mode 100644 index 00000000..b7c63091 --- /dev/null +++ b/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js @@ -0,0 +1,143 @@ +// @flow +import {act} from 'react-hooks-testing-library'; +import {renderHook} from 'react-hooks-testing-library'; +import useParcelBufferInternalKeepValue from '../useParcelBufferInternalKeepValue'; +import Parcel from 'dataparcels'; + +const renderHookWithProps = (initialProps, callback) => renderHook(callback, {initialProps}); + +const value = { + abc: 123, + def: 456 +}; + +describe('useParcelBufferInternalKeepValue should work', () => { + + it('should return false when keepValue is false and change comes from self', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^.abc"; + + let {result} = renderHook(() => useParcelBufferInternalKeepValue({ + keepValue: false, + parcel + })); + + expect(result.current).toBe(false); + }); + + it('should return true when keepValue is true and change comes from self', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^.abc"; + + let {result} = renderHook(() => useParcelBufferInternalKeepValue({ + keepValue: true, + parcel + })); + + expect(result.current).toBe(true); + }); + + it('should return true when keepValue is true and change comes from within self', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^.abc.a"; + + let {result} = renderHook(() => useParcelBufferInternalKeepValue({ + keepValue: true, + parcel + })); + + expect(result.current).toBe(true); + }); + + it('should return false when keepValue is true and change comes from elsewhere', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^"; + + let {result} = renderHook(() => useParcelBufferInternalKeepValue({ + keepValue: true, + parcel + })); + + expect(result.current).toBe(false); + }); + + it('should return true when a change from elsewhere contains the same value as the last change that came from self', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^.abc"; + + let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBufferInternalKeepValue({ + keepValue: true, + parcel + })); + + expect(result.current).toBe(true); + + act(() => { + // pretend that a another identical change came from 'def' + parcel._lastOriginId = "^.def"; + + rerender({ + parcel + }); + }); + + expect(result.current).toBe(true); + + act(() => { + // pretend that a another change came from 'def', but this time with a changed value + parcel = parcel._changeAndReturn(parcel => parcel.set(124)); + parcel._lastOriginId = "^.def"; + + rerender({ + parcel + }); + }); + + expect(result.current).toBe(false); + }); + + it('should clear any memory of received values if keepValue becomes false', () => { + + let parcel = new Parcel({value}).get('abc'); + parcel._lastOriginId = "^.abc"; + + let {result, rerender} = renderHookWithProps( + { + parcel, + keepValue: true + }, + ({parcel, keepValue}) => useParcelBufferInternalKeepValue({ + keepValue, + parcel + }) + ); + + expect(result.current).toBe(true); + + act(() => { + rerender({ + parcel, + keepValue: false + }); + }); + + act(() => { + // pretend that a change came from 'def' with the same value + // that was recieved when keepValue was last true + parcel._lastOriginId = "^.def"; + + rerender({ + parcel, + keepValue: true + }); + }); + + expect(result.current).toBe(false); + }); + +}); diff --git a/packages/react-dataparcels/src/__test__/useParcelState-test.js b/packages/react-dataparcels/src/__test__/useParcelState-test.js new file mode 100644 index 00000000..be23d059 --- /dev/null +++ b/packages/react-dataparcels/src/__test__/useParcelState-test.js @@ -0,0 +1,182 @@ +// @flow +import {act} from 'react-hooks-testing-library'; +import {renderHook} from 'react-hooks-testing-library'; +import useParcelState from '../useParcelState'; + +const renderHookWithProps = (initialProps, callback) => renderHook(callback, {initialProps}); + +describe('useParcelState should use config.value', () => { + + it('should create a Parcel from value', () => { + let {result} = renderHook(() => useParcelState({value: 123})); + expect(result.current[0].value).toBe(123); + }); + + it('should store the value from first render', () => { + let {result, rerender} = renderHookWithProps({foo: 123}, (props) => useParcelState({ + value: props.foo + })); + + expect(result.current[0].value).toBe(123); + + act(() => { + rerender({foo: 456}); + }); + + expect(result.current[0].value).toBe(123); + }); + + + it('should create a Parcel from value thunk', () => { + let {result} = renderHook(() => useParcelState({value: () => 123})); + expect(result.current[0].value).toBe(123); + }); + + it('should update Parcel', () => { + let {result} = renderHook(() => useParcelState({value: () => 123})); + + act(() => { + result.current[0].set(456); + }); + + expect(result.current[0].value).toBe(456); + }); + +}); + +describe('useParcelState should use config.updateValue', () => { + + it('should take value from second render when updateValue = true', () => { + let {result, rerender} = renderHookWithProps({foo: 123}, (props) => useParcelState({ + value: props.foo, + updateValue: true + })); + + expect(result.current[0].value).toBe(123); + + act(() => { + rerender({foo: 456}); + }); + + expect(result.current[0].value).toBe(456); + }); + +}); + +describe('useParcelState should use config.onChange', () => { + + it('should call onChange with value and change request if provided', () => { + let onChange = jest.fn(); + + let {result} = renderHook(() => useParcelState({ + value: () => 123, + onChange + })); + + act(() => { + result.current[0].set(456); + }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].value).toBe(456); + expect(onChange.mock.calls[0][1].prevData.value).toBe(123); + }); + + it('should not call onChange as a result of updateValue', () => { + let onChange = jest.fn(); + + let {result, rerender} = renderHookWithProps({foo: 123}, (props) => useParcelState({ + value: props.foo, + updateValue: true, + onChange + })); + + expect(result.current[0].value).toBe(123); + + act(() => { + rerender({foo: 456}); + }); + + expect(onChange).not.toHaveBeenCalled(); + }); + +}); + +describe('useParcelState should use config.beforeChange', () => { + + it('should apply single beforeChange to parcel', () => { + let {result} = renderHook(() => useParcelState({ + value: 123, + beforeChange: value => value * 2 + })); + + expect(result.current[0].value).toBe(246); + + act(() => { + result.current[0].set(10); + }); + + expect(result.current[0].value).toBe(20); + + act(() => { + result.current[0].set(100); + }); + + expect(result.current[0].value).toBe(200); + }); + + it('should apply multiple beforeChange to parcel', () => { + let {result} = renderHook(() => useParcelState({ + value: 123, + beforeChange: [ + value => value * 2, + value => value + 5 + ] + })); + + expect(result.current[0].value).toBe(251); + + act(() => { + result.current[0].set(10); + }); + + expect(result.current[0].value).toBe(25); + }); + + it('should apply single beforeChange to update value from props', () => { + let {result, rerender} = renderHookWithProps({foo: 123}, (props) => useParcelState({ + value: props.foo, + updateValue: true, + beforeChange: value => value * 2 + })); + + expect(result.current[0].value).toBe(246); + + act(() => { + rerender({foo: 10}); + }); + + expect(result.current[0].value).toBe(20); + }); + + it('should apply multiple beforeChange to update value from props', () => { + let {result, rerender} = renderHookWithProps({foo: 123}, (props) => useParcelState({ + value: props.foo, + updateValue: true, + beforeChange: [ + value => value * 2, + value => value + 5 + ] + })); + + expect(result.current[0].value).toBe(251); + + act(() => { + rerender({foo: 10}); + }); + + expect(result.current[0].value).toBe(25); + }); + +}); + diff --git a/packages/react-dataparcels/src/useParcelBuffer.js b/packages/react-dataparcels/src/useParcelBuffer.js new file mode 100644 index 00000000..5b64a4f4 --- /dev/null +++ b/packages/react-dataparcels/src/useParcelBuffer.js @@ -0,0 +1,162 @@ +// @flow + +import type ChangeRequest from 'dataparcels/ChangeRequest'; +import type {ParcelValueUpdater} from 'dataparcels'; + +// $FlowFixMe - useState is a named export of react +import {useState} from 'react'; +import pipe from 'unmutable/pipe'; + +import useDebouncedCallback from 'use-debounce/lib/callback'; + +import Parcel from 'dataparcels'; + +import ParcelBufferControl from './ParcelBufferControl'; +import ApplyBeforeChange from './util/ApplyBeforeChange'; +import ParcelBoundaryEquals from './util/ParcelBoundaryEquals'; +import pipeWithFakePrevParcel from './util/pipeWithFakePrevParcel'; +import useParcelBufferInternalBuffer from './useParcelBufferInternalBuffer'; +import useParcelBufferInternalKeepValue from './useParcelBufferInternalKeepValue'; + +type Params = { + parcel: Parcel, + debounce?: number, + hold?: boolean, + keepValue?: boolean, + beforeChange?: ParcelValueUpdater|ParcelValueUpdater[] +}; + +type Return = [Parcel, ParcelBufferControl]; + +export default (params: Params): Return => { + + const beforeChange = [].concat(params.beforeChange || []); + const applyBeforeChange = ApplyBeforeChange(beforeChange); + + // + // parcel state and change logic + // + + // outerParcel is locked to changes in props + const [outerParcel, setOuterParcel] = useState(null); + // innerParcel can deviate from props + const [innerParcel, _setInnerParcel] = useState(null); + // shouldKeepValue + const shouldKeepValue = useParcelBufferInternalKeepValue(params); + + // + // inner parcel prep + // + + // always apply applyBeforeChange to inner parcel + // so changes to innerParcel go up through beforeChange + const setInnerParcel = pipe( + applyBeforeChange, + _setInnerParcel + ); + + const prepareKeepValue = (parcel: Parcel): Parcel => { + // set existing value and child on the new parcel from props + let data = { + ...parcel.data, + value: innerParcel.value, + child: innerParcel.child + }; + + return parcel._changeAndReturn( + parcel => parcel._setData(data) + ); + }; + + const prepareInnerParcelFromOuter = () => { + // if keepValue is used, beforeChange is ignored + if(params.keepValue) { + return shouldKeepValue ? prepareKeepValue : parcel => parcel; + } + return pipeWithFakePrevParcel(outerParcel, applyBeforeChange); + // ^ this runs newOuterParcel through beforeChange immediately + // shoving lastReceivedOuterParcel in as a fake previous value + }; + + // + // buffer ref and functions + // + + const { + bufferState, + push, + reset, + submit + } = useParcelBufferInternalBuffer({ + onRelease: (changeRequest) => params.parcel.dispatch(changeRequest), + onClear: () => setOuterParcel(null) + // ^ triggers innerParcel to be recreated from outerParcel + }); + + // debounce + + const [ + debounceRelease, + /* blank */, + callDebounceRelease + ] = useDebouncedCallback(submit, params.debounce); + + if(!params.debounce) { + callDebounceRelease(); + } + + // + // update inner parcel when outer parcel changes (or is first set) + // + + let newInnerParcel; + if(!outerParcel || !ParcelBoundaryEquals(params.parcel, outerParcel)) { + + const handleChange = (newParcel: Parcel, changeRequest: ChangeRequest) => { + const {debounce, hold} = params; + + // push change into the buffer + push(changeRequest); + + if(!debounce && !hold) { + // if no debounce or hold, just propagate it immediately + submit(); + // if keepValue is true, the buffer is in change of its own state + // rather than waiting for new props containing the new value + params.keepValue && setInnerParcel(newParcel); + return; + } + + // if debounce or hold, update inner parcel immediately + // and request a debounced submit if debounce is set + setInnerParcel(newParcel); + debounce && debounceRelease(); + }; + + const newOuterParcel = params.parcel; + + // boundary split to ensure that inner parcels chain are + // completely isolated from outer parcels chain + newInnerParcel = params.parcel + ._boundarySplit({handleChange}) + .pipe(prepareInnerParcelFromOuter()); + + setInnerParcel(newInnerParcel); + setOuterParcel(newOuterParcel); + } + + // + // parcel buffer control + // + + let actions = bufferState ? bufferState.actions : []; + + const parcelBufferControl = new ParcelBufferControl({ + submit, + reset, + buffered: actions.length > 0, + actions + }); + + return [innerParcel || newInnerParcel, parcelBufferControl]; +}; diff --git a/packages/react-dataparcels/src/useParcelBufferInternalBuffer.js b/packages/react-dataparcels/src/useParcelBufferInternalBuffer.js new file mode 100644 index 00000000..b0208dc4 --- /dev/null +++ b/packages/react-dataparcels/src/useParcelBufferInternalBuffer.js @@ -0,0 +1,64 @@ +// @flow + +import type ChangeRequest from 'dataparcels/ChangeRequest'; + +// $FlowFixMe - useState is a named export of react +import {useRef} from 'react'; +// $FlowFixMe - useState is a named export of react +import {useState} from 'react'; + +type Params = { + onRelease: (changeRequest: ChangeRequest) => void, + onClear: () => void +}; + +type Return = { + bufferState: ?ChangeRequest, + push: Function, + reset: Function, + submit: Function +}; + +export default ({onRelease, onClear}: Params): Return => { + + // buffer ref is used to allow handleChange functions from past renders + // to always affect the same buffer reference + const bufferRef = useRef(); + + // buffer state is used to update the component + // whenever the buffer changes so that the buffer + // can be passed down as props + const [bufferState, setBufferState] = useState(null); + + const updateBufferState = () => setBufferState(bufferRef.current); + + const push = (changeRequest: ChangeRequest) => { + bufferRef.current = bufferRef.current + ? bufferRef.current.merge(changeRequest) + : changeRequest; + + updateBufferState(); + }; + + const reset = () => { + bufferRef.current = null; + updateBufferState(); + onClear(); + }; + + const submit = () => { + if(bufferRef.current) { + let changeRequest = bufferRef.current; + onRelease(changeRequest); + bufferRef.current = null; + updateBufferState(); + } + }; + + return { + bufferState, + push, + reset, + submit + }; +}; diff --git a/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js b/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js new file mode 100644 index 00000000..694cbdd2 --- /dev/null +++ b/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js @@ -0,0 +1,48 @@ +// @flow + +import type Parcel from 'dataparcels'; + +// $FlowFixMe - useState is a named export of react +import {useRef} from 'react'; + +type Params = { + keepValue?: boolean, + parcel: Parcel +}; + +const NO_KEEP_VALUE = Symbol('NO_KEEP_VALUE'); + +export default ({keepValue, parcel}: Params): boolean => { + let keepValueReceivedRef = useRef(NO_KEEP_VALUE); + + if(!keepValue) { + keepValueReceivedRef.current = NO_KEEP_VALUE; + return false; + } + + + let changedBySelf = parcel._lastOriginId.startsWith(parcel.id); + //console.log("--"); + //console.log("parcel._lastOriginId", parcel._lastOriginId); + //console.log("parcel.id", parcel.id); + //console.log("changedBySelf", changedBySelf); + if(changedBySelf) { + keepValueReceivedRef.current = parcel.value; + return true; + } + + let valueIsSameAsLastChangeBySelf = Object.is( + parcel.value, + keepValueReceivedRef.current + ); + + //console.log("value", parcel.value); + //console.log("keepValueReceivedRef.current", keepValueReceivedRef.current); + //console.log("valueIsSameAsLastChangeBySelf", valueIsSameAsLastChangeBySelf); + + if(!valueIsSameAsLastChangeBySelf) { + keepValueReceivedRef.current = parcel.value; + } + + return valueIsSameAsLastChangeBySelf; +}; diff --git a/packages/react-dataparcels/src/useParcelState.js b/packages/react-dataparcels/src/useParcelState.js new file mode 100644 index 00000000..fb7ed642 --- /dev/null +++ b/packages/react-dataparcels/src/useParcelState.js @@ -0,0 +1,66 @@ +// @flow + +import type ChangeRequest from 'dataparcels/ChangeRequest'; +import type {ParcelValueUpdater} from 'dataparcels'; + +// $FlowFixMe - useState is a named export of react +import {useState} from 'react'; +import Parcel from 'dataparcels'; +import ApplyBeforeChange from './util/ApplyBeforeChange'; + +type Params = { + value: any, + updateValue?: boolean, + onChange?: (value: any, changeRequest: ChangeRequest) => void, + beforeChange?: ParcelValueUpdater|ParcelValueUpdater[] +}; + +type Return = [Parcel]; + +export default (params: Params): Return => { + + const getValue = typeof params.value === "function" ? params.value : () => params.value; + + // takes a parcel and chains the beforeChange functions off of it + // placing them each inside .modifyUp()s + // so that all changes made to the returned parcel + // go up through all of beforeChange's functions + + let beforeChange = [].concat(params.beforeChange || []); + + const applyBeforeChange = ApplyBeforeChange(beforeChange); + + // takes a parcel and sets the current params.value as its value + // (params.value is first passed through beforeChange), + // then passes the resulting parcel through beforeChange + // so that all changes made to the returned parcel + // go up through all of beforeChange's functions + + const updateParcelValue = (parcel: Parcel): Parcel => applyBeforeChange( + parcel._changeAndReturn( + parcel => applyBeforeChange(parcel).set(getValue()) + ) + ); + + const [parcel, setParcel] = useState(() => updateParcelValue( + new Parcel({ + handleChange: (parcel: Parcel, changeRequest: ChangeRequest) => { + setParcel(applyBeforeChange(parcel)); + params.onChange && params.onChange(parcel, changeRequest); + } + }) + )); + + const [prevValue, setPrevValue] = useState(() => parcel.value); + + if(params.updateValue) { + const value = getValue(); + + if(!Object.is(value, prevValue)) { + setPrevValue(value); + setParcel(updateParcelValue(parcel)); + } + } + + return [parcel]; +}; diff --git a/packages/react-dataparcels/src/util/ApplyBeforeChange.js b/packages/react-dataparcels/src/util/ApplyBeforeChange.js new file mode 100644 index 00000000..ab71a5ee --- /dev/null +++ b/packages/react-dataparcels/src/util/ApplyBeforeChange.js @@ -0,0 +1,10 @@ +// @flow +import type {ParcelValueUpdater} from 'dataparcels'; +import type Parcel from 'dataparcels'; + +export default (beforeChange: ParcelValueUpdater[]) => { + return (parcel: Parcel): Parcel => beforeChange.reduceRight( + (parcel, updater) => parcel.modifyUp(updater), + parcel + ); +}; diff --git a/packages/react-dataparcels/src/util/ApplyModifyBeforeUpdate.js b/packages/react-dataparcels/src/util/ApplyModifyBeforeUpdate.js deleted file mode 100644 index 14bf6550..00000000 --- a/packages/react-dataparcels/src/util/ApplyModifyBeforeUpdate.js +++ /dev/null @@ -1,8 +0,0 @@ -// @flow -import type {ParcelValueUpdater} from 'dataparcels'; - -import compose from 'unmutable/compose'; - -export default (modifyBeforeUpdate: Array) => compose( - ...modifyBeforeUpdate.map((fn) => parcel => parcel.modifyUp(fn)) -); diff --git a/packages/react-dataparcels/src/util/__test__/pipeWithFakePrevParcel-test.js b/packages/react-dataparcels/src/util/__test__/pipeWithFakePrevParcel-test.js new file mode 100644 index 00000000..237936b7 --- /dev/null +++ b/packages/react-dataparcels/src/util/__test__/pipeWithFakePrevParcel-test.js @@ -0,0 +1,28 @@ +// @flow +import Parcel from 'dataparcels'; +import pipeWithFakePrevParcel from '../pipeWithFakePrevParcel'; + +test('pipeWithFakePrevParcel should fakely set a previous', () => { + + let parcel = new Parcel({ + value: 123 + }); + + let fakePrevParcel = new Parcel({ + value: 100 + }); + + let modifyUp = jest.fn(value => value * 2); + + let newParcel = parcel.pipe(pipeWithFakePrevParcel( + fakePrevParcel, + parcel => parcel.modifyUp(modifyUp) + )); + + let [modifyUpValue, changeRequest] = modifyUp.mock.calls[0]; + + expect(newParcel.value).toBe(246); + expect(modifyUpValue).toBe(123); + expect(changeRequest.prevData.value).toBe(100); + expect(changeRequest.nextData.value).toBe(123); +}); diff --git a/packages/react-dataparcels/src/util/pipeWithFakePrevParcel.js b/packages/react-dataparcels/src/util/pipeWithFakePrevParcel.js new file mode 100644 index 00000000..c9a6dfe9 --- /dev/null +++ b/packages/react-dataparcels/src/util/pipeWithFakePrevParcel.js @@ -0,0 +1,22 @@ +// @flow + +import type {ParcelUpdater} from 'dataparcels'; + +import Parcel from 'dataparcels'; +import dangerouslyUpdateParcelData from 'dataparcels/dangerouslyUpdateParcelData'; + +export default (prevParcel: ?Parcel, ...pipe: ParcelUpdater[]) => (parcel: Parcel): Parcel => { + return parcel._changeAndReturn((parcel) => { + + let setPrevParcel; + if(prevParcel) { + let prevParcelData = prevParcel.data; + setPrevParcel = dangerouslyUpdateParcelData(() => prevParcelData); + } + + return parcel + .modifyDown(setPrevParcel || (ii => ii)) + .pipe(...pipe) + ._setData(parcel.data); + }); +}; diff --git a/packages/react-dataparcels/useParcelBuffer.js b/packages/react-dataparcels/useParcelBuffer.js new file mode 100644 index 00000000..40116818 --- /dev/null +++ b/packages/react-dataparcels/useParcelBuffer.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +module.exports = require('./lib/useParcelBuffer.js'); diff --git a/packages/react-dataparcels/useParcelState.js b/packages/react-dataparcels/useParcelState.js new file mode 100644 index 00000000..0ef454f6 --- /dev/null +++ b/packages/react-dataparcels/useParcelState.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +module.exports = require('./lib/useParcelState.js'); diff --git a/packages/react-dataparcels/yarn.lock b/packages/react-dataparcels/yarn.lock index 0267be74..68247086 100644 --- a/packages/react-dataparcels/yarn.lock +++ b/packages/react-dataparcels/yarn.lock @@ -688,7 +688,7 @@ pirates "^4.0.0" source-map-support "^0.5.9" -"@babel/runtime@^7.1.5", "@babel/runtime@^7.2.0": +"@babel/runtime@^7.1.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.2": version "7.4.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc" dependencies: @@ -6489,6 +6489,12 @@ react-dom@^16.8.6: prop-types "^15.6.2" scheduler "^0.13.6" +react-hooks-testing-library@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.5.0.tgz#571af3522f88ea4ac23c634fb4deff84873f2bc2" + dependencies: + "@babel/runtime" "^7.4.2" + react-is@^16.8.1, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" @@ -7756,6 +7762,10 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-debounce@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-1.1.3.tgz#08b76fd8cc9b107e0a6d6ee8bb6c5fe97a14732f" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"