diff --git a/packages/dataparcels-docs/src/content/API.js b/packages/dataparcels-docs/src/content/API.js index 7d30c6ee..f2f4f55c 100644 --- a/packages/dataparcels-docs/src/content/API.js +++ b/packages/dataparcels-docs/src/content/API.js @@ -42,34 +42,27 @@ export default () => } image={IconParcelBoundary} /> - - useParcelState is a React hook. - Its job is to provide a parcel stored in state, and to handle how the parcel responds to changes in React props. - } - image={IconParcelHoc} - /> useParcelForm is a React hook. - Its job is to make submittable forms easy to build, by combining useParcelState and useParcelBuffer together. + Its job is to make submittable forms easy to build. It provides a parcel stored in state and a buffer to store unsaved changes, and also handles how the parcel responds to changes in React props. } - image={IconParcelBoundaryHoc} + image={IconParcelHoc} /> - useParcelBuffer is a React hook. - Its job is to control the flow of parcel changes by providing a buffer. + useParcelState is a React hook. + Its job is to provide a parcel stored in state, and to handle how the parcel responds to changes in React props. } - image={IconParcelBoundaryHoc} + image={IconParcelHoc} /> See also ParcelDrag validation + useParcelBuffer ChangeRequest CancelActionMarker ParcelShape diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChange.jsx b/packages/dataparcels-docs/src/examples/SubmitButtonOnChange.jsx index 12a2150a..2b10ece8 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChange.jsx +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChange.jsx @@ -25,7 +25,7 @@ export default function SignUpForm(props) { let [personParcel, personParcelControl] = useParcelForm({ value: initialValue, - onChange: (parcel) => saveMyData(parcel.value) + onSubmit: (parcel) => saveMyData(parcel.value) // ^ returns a promise }); @@ -43,8 +43,8 @@ export default function SignUpForm(props) { -

Request state: {personParcelControl.onChangeStatus.status} - {personParcelControl.onChangeStatus.isPending && } +

Request state: {personParcelControl.submitStatus.status} + {personParcelControl.submitStatus.isPending && }

); } diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClear.jsx b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClear.jsx index e35ecef3..fea32555 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClear.jsx +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClear.jsx @@ -25,11 +25,11 @@ export default function SignUpForm(props) { let [personParcel, personParcelControl] = useParcelForm({ value: initialValue, - onChange: async (parcel) => { + onSubmit: async (parcel) => { await saveMyData(parcel.value); return initialValue; }, - onChangeUseResult: true + onSubmitUseResult: true }); let personParcelState = personParcelControl._outerParcel; @@ -46,8 +46,8 @@ export default function SignUpForm(props) { -

Request state: {personParcelControl.onChangeStatus.status} - {personParcelControl.onChangeStatus.isPending && } +

Request state: {personParcelControl.submitStatus.status} + {personParcelControl.submitStatus.isPending && }

); } diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClearSource.txt b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClearSource.txt index 8b06dbb0..d9a5853e 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClearSource.txt +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeClearSource.txt @@ -11,11 +11,11 @@ export default function SignUpForm(props) { let [personParcel, personParcelControl] = useParcelForm({ value: initialValue, - onChange: async (parcel) => { + onSubmit: async (parcel) => { await saveMyData(parcel.value); return initialValue; }, - onChangeUseResult: true + onSubmitUseResult: true }); return
diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoad.jsx b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoad.jsx index 3349ce49..00ad9b1f 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoad.jsx +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoad.jsx @@ -32,8 +32,8 @@ export default function PersonEditor(props) { let [personParcel, personParcelControl] = useParcelForm({ value: initialValue, - onChange: (parcel) => saveMyData(parcel.value), - onChangeUseResult: true + onSubmit: (parcel) => saveMyData(parcel.value), + onSubmitUseResult: true }); let {timeUpdated} = personParcel.value; @@ -54,8 +54,8 @@ export default function PersonEditor(props) { -

Request state: {personParcelControl.onChangeStatus.status} - {personParcelControl.onChangeStatus.isPending && } +

Request state: {personParcelControl.submitStatus.status} + {personParcelControl.submitStatus.isPending && }

); } diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoadSource.txt b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoadSource.txt index df562256..755148ce 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoadSource.txt +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeLoadSource.txt @@ -6,8 +6,8 @@ export default function PersonEditor(props) { let [personParcel, personParcelControl] = useParcelForm({ value: props.valueLoadedFromServer, - onChange: (parcel) => saveMyData(parcel.value), - onChangeUseResult: true + onSubmit: (parcel) => saveMyData(parcel.value), + onSubmitUseResult: true }); let {timeUpdated} = personParcel.value; diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeReduxSource.txt b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeReduxSource.txt index 3b49b357..9d3fc86c 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeReduxSource.txt +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeReduxSource.txt @@ -7,7 +7,7 @@ export default function PersonEditor(props) { let [personParcel, personParcelControl] = useParcelForm({ value: props.personData, updateValue: true, - onChange: (parcel) => props.dispatchMySaveAction(parcel.value) + onSubmit: (parcel) => props.dispatchMySaveAction(parcel.value) }); // ^ dispatchMySaveAction should return a promise if it is diff --git a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeSource.txt b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeSource.txt index ef7c8921..2aa60d7c 100644 --- a/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeSource.txt +++ b/packages/dataparcels-docs/src/examples/SubmitButtonOnChangeSource.txt @@ -11,7 +11,7 @@ export default function SignUpForm(props) { let [personParcel, personParcelControl] = useParcelForm({ value: initialValue, - onChange: (parcel) => saveMyData(parcel.value) + onSubmit: (parcel) => saveMyData(parcel.value) // ^ returns a promise }); diff --git a/packages/dataparcels-docs/src/layout/index.scss b/packages/dataparcels-docs/src/layout/index.scss index 1498e53d..68135c2c 100644 --- a/packages/dataparcels-docs/src/layout/index.scss +++ b/packages/dataparcels-docs/src/layout/index.scss @@ -75,6 +75,9 @@ text-shadow: -6px 6px 0px lighten(color('primary'), 10), -12px 12px 0px lighten(color('primary'), 30) } + &-code { + overflow: auto; + } } @include DcmeTypography { input { diff --git a/packages/dataparcels-docs/src/pages/api/useParcelForm.jsx b/packages/dataparcels-docs/src/pages/api/useParcelForm.jsx index ef2494b8..77926b28 100644 --- a/packages/dataparcels-docs/src/pages/api/useParcelForm.jsx +++ b/packages/dataparcels-docs/src/pages/api/useParcelForm.jsx @@ -12,8 +12,8 @@ export default () => '# Params', 'value', 'updateValue', - 'onChange', - 'onChangeUseResult', + 'onSubmit', + 'onSubmitUseResult', 'buffer', 'debounce', 'validation', @@ -21,7 +21,8 @@ export default () => '# Returns', 'parcel', 'parcelControl', - '# ParcelHookControl' + '# ParcelHookControl', + '# Inside the hook' ]} /> ; diff --git a/packages/dataparcels-docs/src/pages/api/useParcelForm.mdx b/packages/dataparcels-docs/src/pages/api/useParcelForm.mdx index 7f879efa..c087eb0f 100644 --- a/packages/dataparcels-docs/src/pages/api/useParcelForm.mdx +++ b/packages/dataparcels-docs/src/pages/api/useParcelForm.mdx @@ -11,48 +11,9 @@ import ValueUpdater from 'docs/notes/ValueUpdater.md'; {Icon} -The useParcelForm function is a React hook. Its job is to make submittable forms easy to build, by combining [useParcelState](/api/useParcelState) and [useParcelBuffer](/api/useParcelBuffer) together. +The useParcelForm function is a React hook. Its job is to make submittable forms easy to build. It provides a parcel stored in state and an internal buffer to store unsaved changes, and also handles how the parcel responds to changes in React props. -This is perfect for creating user interfaces that can show data that's fetched from a server, allow it to be edited by the user, and then send any changes back to the server. - -The useParcelForm hook holds two Parcels in state: -1. the original data provided via `value`, hereby known as "outerParcel" -2. a buffered version of the same Parcel that contains the user's active changes, hereby known as "innerParcel" - -The hook looks roughly like this: - -```js -// 1. Parcel State -// -// holds the original data -// and sends changed data to a callback -let [outerParcel] = useParcelState({ - value, - updateValue -}); - -// ...some magic related to the onChange function... - -// 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 [innerParcel, parcelControl] = useParcelBuffer({ - parcel: outerParcel, - buffer, - debounce, - beforeChange -}); - -// 3. Outside of the useParcelForm hook... -// allow the user to make changes to the data -innerParcel.get('...') // etc - -parcelControl.submit(); // or just use debounce -``` - -Using this pattern, the "submit" button is really an action that instructs the useParcelBuffer hook to release all of its buffered changes up into the useParcelState hook. +This is perfect for creating user interfaces that allow the user to edit data and send changes back to the server. ```js import useParcelForm from 'react-dataparcels/useParcelForm'; @@ -63,8 +24,8 @@ let [parcel] = useParcelForm({ value: any, // optional updateValue?: boolean, - onChange?: Function, - onChangeUseResult?: boolean, + onSubmit?: Function, + onSubmitUseResult?: boolean, buffer?: boolean, debounce?: number, validation?: Function, @@ -72,6 +33,18 @@ let [parcel] = useParcelForm({ }); ``` +The explanations on this page sometimes refer to an "outerParcel" and an "innerParcel". This is because the useParcelForm hook actually holds two Parcels in state: + +#### outerParcel + +The original data provided via `value`. This parcel updates less frequently than innerParcel, only updating when the form is submitted, or if it is instructed to receive a new value via props or via the `onSubmit` function. + +#### innerParcel + +A parcel that sits downstream of outerParcel, acting as a buffer to hold on to unsaved changes. It updates each time the user changes the form, or as a result of outerParcel updating. + +If you're interested you can [read more about what's inside the hook](#Inside-the-hook). + ## Params @@ -136,10 +109,10 @@ parcel.set(200); // then parcel.value is now 300 ``` -### onChange +### onSubmit ```flow -onChange?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise // optional +onSubmit?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise // optional ``` If provided, this function is called after innerParcel releases the contents of its buffer and propagates its changes, but before outerParcel's state is updated. It receives the new [Parcel](/api/Parcel), and the [ChangeRequest](/api/ChangeRequest) that was responsible for the change. This function can be used to relay changes further up the React heirarchy. @@ -147,42 +120,42 @@ If provided, this function is called after innerParcel releases the contents of ```js let [parcel] = useParcelForm({ value: receivedValue, - onChange: (parcel, changeRequest) => { + onSubmit: (parcel, changeRequest) => { // add logic here } }); ``` -#### onChange with promises +#### onSubmit with promises -It's possible to return a promise from `onChange`. When doing this, the ChangeRequest's propagation is halted and is only released once the promise resolves, at which point outerParcel's state will be updated to contain the new Parcel. +It's possible to return a promise from `onSubmit`. When doing this, the ChangeRequest's propagation is halted and is only released once the promise resolves, at which point outerParcel's state will be updated to contain the new Parcel. -If another change arrives while a promise is pending, it will be passed through `onChange` after the first promise is resolved or rejected. This is to ensure that there is only one operation happening at a time. If the first ChangeRequest's -promise is rejected, the changes will be merged with the next ChangeRequest when `onChange` is called the second time. +If another change arrives while a promise is pending, it will be passed through `onSubmit` after the first promise is resolved or rejected. This is to ensure that there is only one operation happening at a time. If the first ChangeRequest's +promise is rejected, the changes will be merged with the next ChangeRequest when `onSubmit` is called the second time. This is discussed in more detail in [data synchronisation](/data-synchronisation). -*Please keep in mind that it is possible for a change to result in the same data being contained in the Parcel, `onChange` will not dedupe subsequent calls whose Parcels contain the same data.* +*Please keep in mind that it is possible for a change to result in the same data being contained in the Parcel, `onSubmit` will not dedupe subsequent calls whose Parcels contain the same data.* -### onChangeUseResult +### onSubmitUseResult ```flow -onChangeUseResult?: boolean = false // optional +onSubmitUseResult?: boolean = false // optional ``` -When true, this sets the value of the outerParcel to the return value of `onChange`. If `onChange` returns a promise, the resolved value of the promise will be used. +When true, this sets the value of the outerParcel to the return value of `onSubmit`. If `onSubmit` returns a promise, the resolved value of the promise will be used. -Using `onChangeUseResult` can be useful for receiving data back from a request to write data to a server, as it ensures that outerParcel's value is as up-to-date as possible. This is discussed in more detail in [data synchronisation](/data-synchronisation#Receiving-data-from-the-server-after-saving). +Using `onSubmitUseResult` can be useful for receiving data back from a request to write data to a server, as it ensures that outerParcel's value is as up-to-date as possible. This is discussed in more detail in [data synchronisation](/data-synchronisation#Receiving-data-from-the-server-after-saving). ```js let [parcel] = useParcelForm({ value: receivedValue, - onChange: (parcel, changeRequest) => { + onSubmit: (parcel, changeRequest) => { return saveMyData(parcel.value); // ^ saveMyData send a request to a server to save the data, // and returns a promise containing the updated data from the server }, - onChangeUseResult: true + onSubmitUseResult: true }); ``` @@ -215,7 +188,7 @@ The useParcelForm hooks waits until no new changes have occured for `debounce` n ```js let [parcel] = useParcelForm({ value: receivedValue, - onChange: (parcel, changeRequest) => { + onSubmit: (parcel, changeRequest) => { // add logic here }, debounce: 500 @@ -322,7 +295,7 @@ type ParcelHookControl { reset: Function, buffered: boolean, actions: Action[], - onChangeStatus: { + submitStatus: { status: string pending: boolean, error: any @@ -342,26 +315,71 @@ type ParcelHookControl { * An array of actions that are currently in the buffer. -* - An object containing information about the current state of the execution of the `onChange` function. This is useful if you're using promises with `onChange` and want to conditionally render elements based on the state of the promise. +* + An object containing information about the current state of the execution of the `onSubmit` function. This is useful if you're using promises with `onSubmit` and want to conditionally render elements based on the state of the promise. * Status is always one of four possible string values: - * `"idle"` - no promises have yet been returned from `onChange` - * `"pending"` - if `onChange` returned a promise and that promise is pending. - * `"resolved"` - if the last promise returned from `onChange` was resolved. - * `"rejected"` - if the last promise returned from `onChange` was rejected. + * `"idle"` - no promises have yet been returned from `onSubmit` + * `"pending"` - if `onSubmit` returned a promise and that promise is pending. + * `"resolved"` - if the last promise returned from `onSubmit` was resolved. + * `"rejected"` - if the last promise returned from `onSubmit` was rejected. * - The `isPending` boolean is true if `onChange` returned a promise and that promise is pending, otherwise it is false. + The `isPending` boolean is true if `onSubmit` returned a promise and that promise is pending, otherwise it is false. * - The `isResolved` boolean is true if the last promise returned from `onChange` was resolved. + The `isResolved` boolean is true if the last promise returned from `onSubmit` was resolved. * - The `isRejected` boolean is true if the last promise returned from `onChange` was rejected. + The `isRejected` boolean is true if the last promise returned from `onSubmit` was rejected. * - If the last promise returned from `onChange` was rejected, this contains the rejected promise's payload. + If the last promise returned from `onSubmit` was rejected, this contains the rejected promise's payload. + +## Inside the hook + +The useParcelForm hook is a combination of [useParcelState](/api/useParcelState) and [useParcelBuffer](/api/useParcelBuffer). + +Internally, the hook looks roughly like this: + +```js + +useParcelForm = (hookConfig) => { + + // 1. Parcel State + // + // holds the original data + // and sends changed data to a callback + let [outerParcel] = useParcelState({ + value, + updateValue + }); + + // ...some magic related to the onSubmit function... + + // 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 [innerParcel, parcelControl] = useParcelBuffer({ + parcel: outerParcel, + buffer, + debounce, + beforeChange + }); + + return [innerParcel, parcelControl]; +} + +// 3. Outside of the useParcelForm hook +// allow the user to make changes to the data +let [innerParcel, parcelControl] = useParcelForm(...); + +innerParcel.get('...') // etc +parcelControl.submit(); +``` +The "submit" button is really an action that instructs the useParcelBuffer hook to release all of its buffered changes up into the useParcelState hook. diff --git a/packages/dataparcels-docs/src/pages/data-synchronisation.mdx b/packages/dataparcels-docs/src/pages/data-synchronisation.mdx index 5132814c..7e51a346 100644 --- a/packages/dataparcels-docs/src/pages/data-synchronisation.mdx +++ b/packages/dataparcels-docs/src/pages/data-synchronisation.mdx @@ -26,7 +26,7 @@ This example makes use of the [useParcelForm](/api/useParcelForm) hook. This is ## Clearing a form after submit -You can set the data in the form to something else after it's sent its data by setting [onChangeUseResult](/api/useParcelForm#onChangeUseResult) to true. The form's current state will be replaced by whatever returned from `onChange`. +You can set the data in the form to something else after it's sent its data by setting [onSubmitUseResult](/api/useParcelForm#onSubmitUseResult) to true. The form's current state will be replaced by whatever returned from `onSubmit`. {SubmitButtonOnChangeClearSource} @@ -46,7 +46,7 @@ If you're using something like [Redux](https://redux.js.org/), you'll likely be ### Updating form data via a promise -You may not have centralised state management like Redux in your app. In this case you can get useParcelForm to update based off the return value of the promise returned by the save function. To use this approach, set the [onChangeUseResult](/api/useParcelForm#onChangeUseResult) parameter to true. +You may not have centralised state management like Redux in your app. In this case you can get useParcelForm to update based off the return value of the promise returned by the save function. To use this approach, set the [onSubmitUseResult](/api/useParcelForm#onSubmitUseResult) parameter to true. {SubmitButtonOnChangeLoadSource} diff --git a/packages/dataparcels-docs/src/pages/index.mdx b/packages/dataparcels-docs/src/pages/index.mdx index a1b7cffe..4e7d067a 100644 --- a/packages/dataparcels-docs/src/pages/index.mdx +++ b/packages/dataparcels-docs/src/pages/index.mdx @@ -69,9 +69,6 @@ I hope this library helps solve some front-end problems for you. ### Roadmap -- Add **revertable changes**. The `useParcelState.onChange` callback should be able to return promises, and this can then allow failed `onChange` calls to reinstate unsaved changes in a lower Parcel buffer. This is a crucial feature that will allow for forms to rollback when requests fail. -- Add **data synchronisation docs**, including data sync strategies strategies with examples. -- Add **merge mode** to control how downward changes are accepted into `ParcelBoundary` components. This is required for rekey. - Add **rekey**, which enables changes via props to be merged into buffered changes (i.e. unsaved changes). This will allow multiple editors to alter the same piece of data simultaneously without overwriting. The ability to rebase unsaved changes onto updated data already exists, but rekey is required to make sense of incoming changes via props. - Add **cache**, an option in `useParcelBuffer` to save, reload and clear cached data. This can be used with `localStorage` or similar external storage mechanisms to retain and restore unsaved changes. - Add **production builds**, a proper build process that doesn't rely on minification and dead code elimination being carried out by the containing project's build process. This step will finally allow proper optimisations to reduce bundle size. diff --git a/packages/dataparcels-docs/src/shape/ContentNav.jsx b/packages/dataparcels-docs/src/shape/ContentNav.jsx index 41c68212..ec803bd1 100644 --- a/packages/dataparcels-docs/src/shape/ContentNav.jsx +++ b/packages/dataparcels-docs/src/shape/ContentNav.jsx @@ -15,9 +15,8 @@ const nav = () => API Parcel ParcelBoundary - useParcelState useParcelForm - useParcelBuffer + useParcelState validation ParcelDrag more... diff --git a/packages/dataparcels-docs/yalc.lock b/packages/dataparcels-docs/yalc.lock index 7c7e3edb..22773767 100644 --- a/packages/dataparcels-docs/yalc.lock +++ b/packages/dataparcels-docs/yalc.lock @@ -2,7 +2,7 @@ "version": "v1", "packages": { "react-dataparcels": { - "signature": "d4b85df241289d4830108fcb27e75b96", + "signature": "7640a04beb7f2428153fe8382775b5e7", "file": true, "replaced": "^0.21.0" }, diff --git a/packages/dataparcels-docs/yarn.lock b/packages/dataparcels-docs/yarn.lock index 95daeb27..e3d11add 100644 --- a/packages/dataparcels-docs/yarn.lock +++ b/packages/dataparcels-docs/yarn.lock @@ -3597,10 +3597,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -dataparcels@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/dataparcels/-/dataparcels-0.22.0.tgz#386ad3b072970bc1f5db77fbdaf7d6b1c5fc8e78" - integrity sha512-8LEBB0hVHUwvuDSM3e/ybWiKqZoO8AfiLx7h41f58Ov+pZ7ia+1RCcNNm0+qq1VIK/E7QAYphN8Rybb0iJPyDA== +dataparcels@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/dataparcels/-/dataparcels-0.23.0.tgz#da9e5f4075906f9ea6540ea94130e6ab93746b92" + integrity sha512-hw7K+aTTJzPZmkfU+Ct2W+yyH6092t3bXoQ9p8fSQfAsEEAuiBSUU4eHxlqCOdb7z6wrVXWoLya5/WnA3St3rA== dependencies: "@babel/runtime" "^7.1.5" unmutable "^0.41.1" @@ -9688,16 +9688,16 @@ react-cool-storage@^0.1.1: unmutable "^0.39.0" "react-dataparcels-drag@file:.yalc/react-dataparcels-drag": - version "0.22.0-b07beb78" + version "0.23.0-2bbfb62a" dependencies: "@babel/runtime" "^7.1.5" react-sortable-hoc "1.4.0" "react-dataparcels@file:.yalc/react-dataparcels": - version "0.22.0-c304f58a" + version "0.23.0-640ebd29" dependencies: "@babel/runtime" "^7.1.5" - dataparcels "^0.22.0" + dataparcels "^0.23.0" is-promise "^2.1.0" unmutable "^0.41.1" use-debounce "^1.1.3" diff --git a/packages/dataparcels/src/errors/Errors.js b/packages/dataparcels/src/errors/Errors.js index c6f820af..2f787c72 100644 --- a/packages/dataparcels/src/errors/Errors.js +++ b/packages/dataparcels/src/errors/Errors.js @@ -5,4 +5,3 @@ export const ReducerInvalidActionError = (actionType: string) => new Error(`"${a export const ReducerInvalidStepError = (stepType: string) => new Error(`"${stepType}" is not a valid action step type`); export const ChangeRequestNoPrevDataError = () => new Error(`ChangeRequest data cannot be accessed before setting changeRequest.prevData`); export const ShapeUpdaterNonShapeChildError = () => new Error(`Every child value on a collection returned from a shape updater must be a ParcelShape`); -export const ChangeAndReturnNotCalledError = () => new Error(`_changeAndReturn unchanged`); diff --git a/packages/dataparcels/src/parcel/Parcel.js b/packages/dataparcels/src/parcel/Parcel.js index 5293fca6..a503987f 100644 --- a/packages/dataparcels/src/parcel/Parcel.js +++ b/packages/dataparcels/src/parcel/Parcel.js @@ -17,7 +17,6 @@ import type {ParentType} from '../types/Types'; import Types from '../types/Types'; import {ReadOnlyError} from '../errors/Errors'; -import {ChangeAndReturnNotCalledError} from '../errors/Errors'; import ParcelGetMethods from './methods/ParcelGetMethods'; import ParcelChangeMethods from './methods/ParcelChangeMethods'; @@ -41,7 +40,7 @@ import overload from 'unmutable/lib/util/overload'; const DEFAULT_CONFIG_INTERNAL = () => ({ child: undefined, dispatchId: '', - lastOriginId: '', + frameMeta: {}, meta: {}, id: new ParcelId(), parent: { @@ -67,7 +66,7 @@ export default class Parcel { let { child, dispatchId, - lastOriginId, + frameMeta, meta, id, parent, @@ -75,7 +74,7 @@ export default class Parcel { updateChangeRequestOnDispatch } = _configInternal || DEFAULT_CONFIG_INTERNAL(); - this._lastOriginId = lastOriginId; + this._frameMeta = frameMeta; this._onHandleChange = handleChange; this._updateChangeRequestOnDispatch = updateChangeRequestOnDispatch; @@ -124,15 +123,15 @@ export default class Parcel { // // from constructor - _childParcelCache: { [key: string]: Parcel } = {}; + _childParcelCache: {[key: string]: Parcel} = {}; _dispatchId: string; _id: ParcelId; _isChild: boolean; _isElement: boolean; _isIndexed: boolean; _isParent: boolean; - _lastOriginId: string; - _methods: { [key: string]: * }; + _frameMeta: {[key: string]: any}; + _methods: {[key: string]: any}; _onHandleChange: ?Function; _parcelData: ParcelData; _parent: ParcelParent; @@ -148,7 +147,7 @@ export default class Parcel { dispatchId = this._id.id(), handleChange, id = this._id, - lastOriginId = this._lastOriginId, + frameMeta = this._frameMeta, parcelData = this._parcelData, parent = this._parent, registry = this._registry, @@ -169,7 +168,7 @@ export default class Parcel { { child, dispatchId, - lastOriginId, + frameMeta, meta, id, parent, @@ -188,24 +187,21 @@ export default class Parcel { } }; - _changeAndReturn = (changeCatcher: (parcel: Parcel) => void): [Parcel, ChangeRequest] => { + _changeAndReturn = (changeCatcher: (parcel: Parcel) => void): [Parcel, ?ChangeRequest] => { let result; - let {_onHandleChange, _lastOriginId} = this; + let {_onHandleChange} = this; // swap out the parcels real _onHandleChange with a spy this._onHandleChange = (parcel, changeRequest) => { - // _changeAndReturn should not alter _lastOriginId - // as it's never triggered by a user action - // so revert to the current parcel's _lastOriginId parcel._onHandleChange = _onHandleChange; - parcel._lastOriginId = _lastOriginId; + parcel._frameMeta = this._frameMeta; result = [parcel, changeRequest]; }; changeCatcher(this); this._onHandleChange = _onHandleChange; if(!result) { - throw ChangeAndReturnNotCalledError(); + return [this, undefined]; } return result; }; diff --git a/packages/dataparcels/src/parcel/__test__/Parcel-test.js b/packages/dataparcels/src/parcel/__test__/Parcel-test.js index 34c5bddf..36f4d4f8 100644 --- a/packages/dataparcels/src/parcel/__test__/Parcel-test.js +++ b/packages/dataparcels/src/parcel/__test__/Parcel-test.js @@ -36,7 +36,6 @@ test('Parcel._changeAndReturn() should call action and return Parcel', () => { }, handleChange }); - parcel._lastOriginId = "foo"; let [newParcel] = parcel._changeAndReturn((parcel) => { parcel.get('abc').onChange(789); @@ -59,12 +58,11 @@ test('Parcel._changeAndReturn() should call action and return Parcel', () => { newParcel.get('abc').onChange(100); expect(handleChange).toHaveBeenCalledTimes(2); - // _changeAndReturn should not affect parcel._lastOriginId as it is an internal function - // that never corresponds to actions triggered by user input - expect(newParcel._lastOriginId).toBe("foo"); + // _frameMeta should be passed through + expect(parcel._frameMeta).toBe(newParcel._frameMeta); }); -test('Parcel._changeAndReturn() should throw error if no changes are made', () => { +test('Parcel._changeAndReturn() should return [parcel, undefined] if no changes are made', () => { let handleChange = jest.fn(); let parcel = new Parcel({ @@ -75,7 +73,9 @@ test('Parcel._changeAndReturn() should throw error if no changes are made', () = handleChange }); - expect(() => parcel._changeAndReturn((parcel) => {})).toThrow("_changeAndReturn unchanged"); + let result = parcel._changeAndReturn(() => {}); + + expect(result).toEqual([parcel, undefined]); }); test('Parcel types should correctly identify primitive values', () => { @@ -267,3 +267,28 @@ test('Correct methods are created for array element values', () => { expect(() => new Parcel(data).get(0).swapNext()).not.toThrow(); }); +test('Frame meta should be passed down to child parcels', () => { + let parcel = new Parcel({ + value: [[123]] + }); + + parcel._frameMeta.foo = 123; + + expect(parcel.get(0)._frameMeta.foo).toBe(123); + expect(parcel.get(0).get(0)._frameMeta.foo).toBe(123); +}); + +test('Frame meta should not persist after change', () => { + let handleChange = jest.fn(); + + let parcel = new Parcel({ + value: 123, + handleChange + }); + + parcel._frameMeta.foo = "bar"; + parcel.set(456); + + expect(handleChange.mock.calls[0][0]._frameMeta).toEqual({}); +}); + diff --git a/packages/dataparcels/src/parcel/methods/ParcelChangeMethods.js b/packages/dataparcels/src/parcel/methods/ParcelChangeMethods.js index e2c296e4..d1e85abd 100644 --- a/packages/dataparcels/src/parcel/methods/ParcelChangeMethods.js +++ b/packages/dataparcels/src/parcel/methods/ParcelChangeMethods.js @@ -29,6 +29,12 @@ export default (_this: Parcel) => ({ changeRequest._originPath = _this.path; } + // clear changeRequest's cache + changeRequest = changeRequest._create({ + prevData: undefined, + nextData: undefined + }); + if(process.env.NODE_ENV !== 'production' && _this._log) { console.log(`Parcel: "${_this._logName}" data up:`); // eslint-disable-line console.log(changeRequest.toJS()); // eslint-disable-line @@ -47,7 +53,7 @@ export default (_this: Parcel) => ({ let parcelWithChangedData = _this._create({ handleChange: _onHandleChange, parcelData, - lastOriginId: changeRequest.originId + frameMeta: {} }); _onHandleChange(parcelWithChangedData, changeRequestWithBase); diff --git a/packages/dataparcels/src/types/Types.js b/packages/dataparcels/src/types/Types.js index e98a816e..afeca237 100644 --- a/packages/dataparcels/src/types/Types.js +++ b/packages/dataparcels/src/types/Types.js @@ -31,7 +31,7 @@ export type ParcelConfigInternal = { child: *, dispatchId: string, id: ParcelId, - lastOriginId: string, + frameMeta: {[key: string]: any}, meta: ParcelMeta, parent: ParcelParent, registry: ParcelRegistry, @@ -40,7 +40,7 @@ export type ParcelConfigInternal = { export type ParcelCreateConfigType = { dispatchId?: string, - lastOriginId?: string, + frameMeta?: {[key: string]: any}, id?: ParcelId, handleChange?: Function, parcelData?: ParcelData, diff --git a/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx b/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx index 66e90504..69a0e2d7 100644 --- a/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx +++ b/packages/react-dataparcels/src/ParcelBoundaryDeprecated.jsx @@ -125,7 +125,7 @@ export default class ParcelBoundary extends React.Component { /* e let newData = parcel.data; if(keepValue) { - let changedBySelf = parcel._lastOriginId.startsWith(parcel.id); + let changedBySelf = parcel._frameMeta.lastOriginId.startsWith(parcel.id); if(changedBySelf) { newState.lastValueFromSelf = parcel.value; } diff --git a/packages/react-dataparcels/src/ParcelHoc.jsx b/packages/react-dataparcels/src/ParcelHoc.jsx index fb1863d3..163be417 100644 --- a/packages/react-dataparcels/src/ParcelHoc.jsx +++ b/packages/react-dataparcels/src/ParcelHoc.jsx @@ -138,6 +138,10 @@ export default (config: ParcelHocConfig): Function => { } handleChange = (parcel: Parcel, changeRequest: ChangeRequest) => { + parcel._frameMeta = { + lastOriginId: changeRequest.originId + }; + this.setState({parcel}); if(process.env.NODE_ENV !== 'production' && debugParcel) { console.log(`ParcelHoc: Parcel changed:`); // eslint-disable-line diff --git a/packages/react-dataparcels/src/__test__/ParcelBoundaryDeprecated-test.js b/packages/react-dataparcels/src/__test__/ParcelBoundaryDeprecated-test.js index 21ab1af3..20e2a307 100644 --- a/packages/react-dataparcels/src/__test__/ParcelBoundaryDeprecated-test.js +++ b/packages/react-dataparcels/src/__test__/ParcelBoundaryDeprecated-test.js @@ -467,6 +467,9 @@ test('ParcelBoundary should not update value from props for updates caused by th childParcel.onChange(456); let newParcel = handleChange.mock.calls[0][0]; + newParcel._frameMeta = { + lastOriginId: handleChange.mock.calls[0][1].originId + }; // verify that the current value of the parcel has been updated expect(newParcel.value).toBe(457); @@ -483,6 +486,9 @@ test('ParcelBoundary should not update value from props for updates caused by th // make a change externally and ensure that the value in the boundary does update newParcel.set(789); let newParcel2 = handleChange.mock.calls[1][0]; + newParcel2._frameMeta = { + lastOriginId: handleChange.mock.calls[1][1].originId + }; wrapper.setProps({ parcel: withModify(newParcel2) }); @@ -510,6 +516,9 @@ test('ParcelBoundary should update meta from props for updates caused by themsel childParcel.onChange(456); let newParcel = handleChange.mock.calls[0][0]; + newParcel._frameMeta = { + lastOriginId: handleChange.mock.calls[0][1].originId + }; // make a change that keepValue will prevent from altering its value wrapper.setProps({ @@ -522,6 +531,9 @@ test('ParcelBoundary should update meta from props for updates caused by themsel abc: 789 }); let newParcel2 = handleChange.mock.calls[1][0]; + newParcel2._frameMeta = { + lastOriginId: handleChange.mock.calls[1][1].originId + }; wrapper.setProps({ parcel: withModify(newParcel2) }); diff --git a/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js b/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js index d173b555..1578a405 100644 --- a/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js +++ b/packages/react-dataparcels/src/__test__/useParcelBuffer-test.js @@ -72,7 +72,7 @@ describe('useParcelBuffer should use config.parcel', () => { expect(result.current[0]).toBe(firstResult); }); - it('should pass new inner parcel if outer parcel is different', () => { + it('should pass new inner parcel and clear buffer contents if outer parcel is different', () => { let parcel = new Parcel({ value: 123 @@ -80,6 +80,10 @@ describe('useParcelBuffer should use config.parcel', () => { let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBuffer({parcel})); + act(() => { + result.current[0].set(124); + }); + act(() => { rerender({ parcel: new Parcel({ @@ -88,7 +92,57 @@ describe('useParcelBuffer should use config.parcel', () => { }); }); + // inner parcel should have outer parcels value expect(result.current[0].value).toEqual(456); + // buffer should be cleared + expect(result.current[1].actions.length).toBe(0); + }); + + it('should keep inner parcel and buffer contents if outer parcel is different and mergeMode is "rebase"', () => { + + let parcel = new Parcel({ + value: { + abc: 100, + def: 100 + } + }); + + let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBuffer({parcel})); + + act(() => { + result.current[0].set('abc', 400); + }); + + // confirm that set() has worked + expect(result.current[0].value).toEqual({ + abc: 400, + def: 100 + }); + + act(() => { + let parcel = new Parcel({ + value: { + abc: 200, + def: 200 + } + }); + + parcel._frameMeta.mergeMode = "rebase"; + + rerender({ + parcel + }); + }); + + // actions should be rebased onto new parcel from props + // and inner parcel should contain the resulting data + expect(result.current[0].value).toEqual({ + abc: 400, + def: 200 + }); + + // buffer should remain + expect(result.current[1].actions.length).toBe(1); }); }); @@ -272,7 +326,7 @@ describe('useParcelBuffer should use config.debounce', () => { }); expect(handleChange.mock.calls[0][0].value).toEqual(["A", "B"]); - expect(handleChange.mock.calls[1][0].value).toEqual(["A", "B", "C"]); + expect(handleChange.mock.calls[1][0].value).toEqual(["C"]); }); it('should flush debounced changes if config.debounce is removed', () => { @@ -440,6 +494,9 @@ describe('useParcelBuffer should use config.keepValue', () => { }); let newParcel = handleChange.mock.calls[0][0]; + newParcel._frameMeta = { + lastOriginId: handleChange.mock.calls[0][1].originId + }; expect(newParcel.value).toEqual({ abc: NaN diff --git a/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js b/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js index 1f638169..fe8f210e 100644 --- a/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js +++ b/packages/react-dataparcels/src/__test__/useParcelBufferInternalKeepValue-test.js @@ -16,7 +16,7 @@ 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"; + parcel._frameMeta.lastOriginId = "^.abc"; let {result} = renderHook(() => useParcelBufferInternalKeepValue({ keepValue: false, @@ -29,7 +29,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { it('should return true when keepValue is true and change comes from self', () => { let parcel = new Parcel({value}).get('abc'); - parcel._lastOriginId = "^.abc"; + parcel._frameMeta.lastOriginId = "^.abc"; let {result} = renderHook(() => useParcelBufferInternalKeepValue({ keepValue: true, @@ -42,7 +42,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { 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"; + parcel._frameMeta.lastOriginId = "^.abc.a"; let {result} = renderHook(() => useParcelBufferInternalKeepValue({ keepValue: true, @@ -55,7 +55,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { it('should return false when keepValue is true and change comes from elsewhere', () => { let parcel = new Parcel({value}).get('abc'); - parcel._lastOriginId = "^"; + parcel._frameMeta.lastOriginId = "^"; let {result} = renderHook(() => useParcelBufferInternalKeepValue({ keepValue: true, @@ -68,7 +68,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { 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"; + parcel._frameMeta.lastOriginId = "^.abc"; let {result, rerender} = renderHookWithProps({parcel}, ({parcel}) => useParcelBufferInternalKeepValue({ keepValue: true, @@ -79,7 +79,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { act(() => { // pretend that a another identical change came from 'def' - parcel._lastOriginId = "^.def"; + parcel._frameMeta.lastOriginId = "^.def"; rerender({ parcel @@ -91,7 +91,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { act(() => { // pretend that a another change came from 'def', but this time with a changed value parcel = parcel._changeAndReturn(parcel => parcel.set(124))[0]; - parcel._lastOriginId = "^.def"; + parcel._frameMeta.lastOriginId = "^.def"; rerender({ parcel @@ -104,7 +104,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { it('should clear any memory of received values if keepValue becomes false', () => { let parcel = new Parcel({value}).get('abc'); - parcel._lastOriginId = "^.abc"; + parcel._frameMeta.lastOriginId = "^.abc"; let {result, rerender} = renderHookWithProps( { @@ -129,7 +129,7 @@ describe('useParcelBufferInternalKeepValue should work', () => { act(() => { // pretend that a change came from 'def' with the same value // that was recieved when keepValue was last true - parcel._lastOriginId = "^.def"; + parcel._frameMeta.lastOriginId = "^.def"; rerender({ parcel, diff --git a/packages/react-dataparcels/src/__test__/useParcelForm-test.js b/packages/react-dataparcels/src/__test__/useParcelForm-test.js index cc202431..dfefb1f2 100644 --- a/packages/react-dataparcels/src/__test__/useParcelForm-test.js +++ b/packages/react-dataparcels/src/__test__/useParcelForm-test.js @@ -24,7 +24,7 @@ describe('useParcelForm should pass config to useParcelState', () => { expect(calledWith.value).toBe(123); expect(calledWith.updateValue).toBe(false); - expect(calledWith.onChange).toBe(undefined); + expect(calledWith.onSubmit).toBe(undefined); }); it('should pass updateValue to useParcelState', () => { @@ -47,28 +47,28 @@ describe('useParcelForm should pass config to useParcelSideEffect', () => { })); expect(getLastCall(useParcelSideEffect)[0].parcel).toBe(getLastResult(useParcelState)[0]); - expect(getLastCall(useParcelSideEffect)[0].onChangeUseResult).toBe(false); + expect(getLastCall(useParcelSideEffect)[0].onSubmitUseResult).toBe(false); }); - it('should pass onChange to useParcelSideEffect', () => { - let onChange = () => {}; + it('should pass onSubmit to useParcelSideEffect', () => { + let onSubmit = () => {}; renderHook(() => useParcelForm({ value: 123, - onChange + onSubmit })); - expect(getLastCall(useParcelSideEffect)[0].onChange).toBe(onChange); + expect(getLastCall(useParcelSideEffect)[0].onSubmit).toBe(onSubmit); }); - it('should pass onChangeUseResult to useParcelSideEffect', () => { + it('should pass onSubmitUseResult to useParcelSideEffect', () => { renderHook(() => useParcelForm({ value: 123, - onChangeUseResult: true + onSubmitUseResult: true })); - expect(getLastCall(useParcelSideEffect)[0].onChangeUseResult).toBe(true); + expect(getLastCall(useParcelSideEffect)[0].onSubmitUseResult).toBe(true); }); }); diff --git a/packages/react-dataparcels/src/__test__/useParcelFormRevert-test.js b/packages/react-dataparcels/src/__test__/useParcelFormRevert-test.js index 525cdd26..acc8c8b1 100644 --- a/packages/react-dataparcels/src/__test__/useParcelFormRevert-test.js +++ b/packages/react-dataparcels/src/__test__/useParcelFormRevert-test.js @@ -6,7 +6,7 @@ import useParcelForm from '../useParcelForm'; describe('useParcelForm should revert change request', () => { - it('should put changes back into buffer from rejected onChange', async () => { + it('should put changes back into buffer from rejected onSubmit', async () => { let rejectMyPromise; let promise = new Promise((resolve, reject) => { rejectMyPromise = () => { @@ -17,7 +17,7 @@ describe('useParcelForm should revert change request', () => { let {result} = renderHook(() => useParcelForm({ value: [], - onChange: () => promise + onSubmit: () => promise })); act(() => { @@ -42,7 +42,7 @@ describe('useParcelForm should revert change request', () => { expect(result.current[1].actions[0]).toBe(firstAction); }); - it('should put changes back into buffer from rejected onChange, onto new changes', async () => { + it('should put changes back into buffer from rejected onSubmit, onto new changes', async () => { let rejectMyPromise; let promise = new Promise((resolve, reject) => { rejectMyPromise = () => { @@ -53,7 +53,7 @@ describe('useParcelForm should revert change request', () => { let {result} = renderHook(() => useParcelForm({ value: [], - onChange: () => promise + onSubmit: () => promise })); act(() => { diff --git a/packages/react-dataparcels/src/__test__/useParcelSideEffect-test.js b/packages/react-dataparcels/src/__test__/useParcelSideEffect-test.js index cc05c773..9ca8f8ed 100644 --- a/packages/react-dataparcels/src/__test__/useParcelSideEffect-test.js +++ b/packages/react-dataparcels/src/__test__/useParcelSideEffect-test.js @@ -2,12 +2,11 @@ import {act} from 'react-hooks-testing-library'; import {renderHook} from 'react-hooks-testing-library'; import useParcelSideEffect from '../useParcelSideEffect'; -import useParcelBuffer from '../useParcelBuffer'; import Parcel from 'dataparcels'; const renderHookWithProps = (initialProps, callback) => renderHook(callback, {initialProps}); -const onChangePromise = (onChange, index = 0) => onChange.mock.results[index].value.catch(() => {}); +const onSubmitPromise = (onSubmit, index = 0) => onSubmit.mock.results[index].value.catch(() => {}); describe('useParcelSideEffect should use config.parcel', () => { @@ -91,11 +90,11 @@ describe('useParcelSideEffect should use config.parcel', () => { }); -describe('useParcelSideEffect should use config.onChange', () => { +describe('useParcelSideEffect should use config.onSubmit', () => { - it('should call onChange with value and change request if provided', () => { + it('should call onSubmit with value and change request if provided', () => { let handleChange = jest.fn(); - let onChange = jest.fn(); + let onSubmit = jest.fn(); let parcel = new Parcel({ value: 123, @@ -104,22 +103,22 @@ describe('useParcelSideEffect should use config.onChange', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); 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); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toBe(456); + expect(onSubmit.mock.calls[0][1].prevData.value).toBe(123); expect(handleChange.mock.calls[0][0].value).toBe(456); }); - it('should call onChange with result if onChangeUseResult = true', () => { + it('should call onSubmit with result if onSubmitUseResult = true', () => { let handleChange = jest.fn(); - let onChange = jest.fn(() => 333); + let onSubmit = jest.fn(() => 333); let parcel = new Parcel({ value: [123], @@ -128,30 +127,30 @@ describe('useParcelSideEffect should use config.onChange', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange, - onChangeUseResult: true + onSubmit, + onSubmitUseResult: true })); act(() => { result.current[0].get(0).set(456); }); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange.mock.calls[0][0].value).toEqual([456]); - expect(onChange.mock.calls[0][1].prevData.value).toEqual([123]); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toEqual([456]); + expect(onSubmit.mock.calls[0][1].prevData.value).toEqual([123]); expect(handleChange.mock.calls[0][0].value).toEqual(333); - expect(handleChange.mock.calls[0][1].originId).toBe(onChange.mock.calls[0][1].originId); + expect(handleChange.mock.calls[0][1].originId).toBe(onSubmit.mock.calls[0][1].originId); }); }); -describe('useParcelSideEffect should use config.onChange with promises', () => { +describe('useParcelSideEffect should use config.onSubmit with promises', () => { - it('should default to a default onChangeStatus', async () => { + it('should default to a default submitStatus', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(() => Promise.resolve(333)); + let onSubmit = jest.fn(() => Promise.resolve(333)); let parcel = new Parcel({ value: 123, @@ -160,11 +159,11 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); expect(result.current[1]).toEqual({ - onChangeStatus: { + submitStatus: { status: 'idle', isPending: false, isResolved: false, @@ -176,9 +175,9 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); - it('should call onChange with promise that resolves', async () => { + it('should call onSubmit with promise that resolves', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(() => Promise.resolve(333)); + let onSubmit = jest.fn(() => Promise.resolve(333)); let parcel = new Parcel({ value: 123, @@ -187,20 +186,20 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); 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); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toBe(456); + expect(onSubmit.mock.calls[0][1].prevData.value).toBe(123); expect(handleChange).toHaveBeenCalledTimes(0); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); expect(handleChange).toHaveBeenCalledTimes(1); @@ -208,7 +207,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); it('should set status correctly with promise that resolves', async () => { - let onChange = jest.fn(() => Promise.resolve(333)); + let onSubmit = jest.fn(() => Promise.resolve(333)); let parcel = new Parcel({ value: 123 @@ -216,7 +215,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); act(() => { @@ -224,7 +223,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); expect(result.current[1]).toEqual({ - onChangeStatus: { + submitStatus: { status: 'pending', isPending: true, isResolved: false, @@ -234,11 +233,11 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); expect(result.current[1]).toEqual({ - onChangeStatus: { + submitStatus: { status: 'resolved', isPending: false, isResolved: true, @@ -248,9 +247,9 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); }); - it('should not call onChange with promise that rejects', async () => { + it('should not call onSubmit with promise that rejects', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(() => Promise.reject('error message!')); + let onSubmit = jest.fn(() => Promise.reject('error message!')); let parcel = new Parcel({ value: 123, @@ -259,20 +258,20 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); 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); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toBe(456); + expect(onSubmit.mock.calls[0][1].prevData.value).toBe(123); expect(handleChange).toHaveBeenCalledTimes(0); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); expect(handleChange).toHaveBeenCalledTimes(0); @@ -280,7 +279,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); it('should set status correctly with promise that rejects', async () => { - let onChange = jest.fn(() => Promise.reject('error message!')); + let onSubmit = jest.fn(() => Promise.reject('error message!')); let parcel = new Parcel({ value: 123 @@ -288,7 +287,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); act(() => { @@ -296,11 +295,11 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); expect(result.current[1]).toEqual({ - onChangeStatus: { + submitStatus: { status: 'rejected', isPending: false, isResolved: false, @@ -310,9 +309,9 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); }); - it('should call onChange with promise that resolves with result if onChangeUseResult = true', async () => { + it('should call onSubmit with promise that resolves with result if onSubmitUseResult = true', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(() => Promise.resolve(333)); + let onSubmit = jest.fn(() => Promise.resolve(333)); let parcel = new Parcel({ value: 123, @@ -321,31 +320,31 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange, - onChangeUseResult: true + onSubmit, + onSubmitUseResult: true })); 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); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toBe(456); + expect(onSubmit.mock.calls[0][1].prevData.value).toBe(123); expect(handleChange).toHaveBeenCalledTimes(0); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); expect(handleChange).toHaveBeenCalledTimes(1); expect(handleChange.mock.calls[0][0].value).toBe(333); }); - it('should merge subsequent onChange if promise resolves twice', async () => { + it('should merge subsequent onSubmit if promise resolves twice', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(); - onChange + let onSubmit = jest.fn(); + onSubmit .mockReturnValueOnce(Promise.resolve()) .mockReturnValueOnce(Promise.resolve()); @@ -356,7 +355,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); act(() => { @@ -365,28 +364,28 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); // only process one change at a time... - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange.mock.calls[0][0].value).toEqual([123]); - expect(onChange.mock.calls[0][1].prevData.value).toEqual([]); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toEqual([123]); + expect(onSubmit.mock.calls[0][1].prevData.value).toEqual([]); // wait until the first promise is complete before firing off anything expect(handleChange).toHaveBeenCalledTimes(0); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange.mock.calls[1][0].value).toEqual([123, 456]); - expect(onChange.mock.calls[1][1].prevData.value).toEqual([]); + expect(onSubmit).toHaveBeenCalledTimes(2); + expect(onSubmit.mock.calls[1][0].value).toEqual([123, 456]); + expect(onSubmit.mock.calls[1][1].prevData.value).toEqual([]); expect(handleChange).toHaveBeenCalledTimes(1); expect(handleChange.mock.calls[0][0].value).toEqual([123, 456]); }); - it('should merge subsequent onChange if promise rejects and then resolves', async () => { + it('should merge subsequent onSubmit if promise rejects and then resolves', async () => { let handleChange = jest.fn(); - let onChange = jest.fn(); - onChange + let onSubmit = jest.fn(); + onSubmit .mockReturnValueOnce(Promise.reject()) .mockReturnValueOnce(Promise.resolve()); @@ -397,7 +396,7 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { let {result} = renderHook(() => useParcelSideEffect({ parcel, - onChange + onSubmit })); act(() => { @@ -406,20 +405,20 @@ describe('useParcelSideEffect should use config.onChange with promises', () => { }); // only process one change at a time... - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange.mock.calls[0][0].value).toEqual([123]); - expect(onChange.mock.calls[0][1].prevData.value).toEqual([]); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].value).toEqual([123]); + expect(onSubmit.mock.calls[0][1].prevData.value).toEqual([]); // wait until the first promise is complete before firing off anything expect(handleChange).toHaveBeenCalledTimes(0); await act(async () => { - await onChangePromise(onChange); + await onSubmitPromise(onSubmit); }); - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange.mock.calls[1][0].value).toEqual([123, 456]); - expect(onChange.mock.calls[1][1].prevData.value).toEqual([]); + expect(onSubmit).toHaveBeenCalledTimes(2); + expect(onSubmit.mock.calls[1][0].value).toEqual([123, 456]); + expect(onSubmit.mock.calls[1][1].prevData.value).toEqual([]); expect(handleChange).toHaveBeenCalledTimes(1); expect(handleChange.mock.calls[0][0].value).toEqual([123, 456]); }); diff --git a/packages/react-dataparcels/src/useParcelBuffer.js b/packages/react-dataparcels/src/useParcelBuffer.js index e7c06163..76e0a322 100644 --- a/packages/react-dataparcels/src/useParcelBuffer.js +++ b/packages/react-dataparcels/src/useParcelBuffer.js @@ -92,8 +92,8 @@ export default (params: Params): Return => { return shouldKeepValue ? prepareKeepValue : parcel => parcel; } return pipeWithFakePrevParcel(outerParcel, applyBeforeChange); - // ^ this runs newOuterParcel through beforeChange immediately - // shoving lastReceivedOuterParcel in as a fake previous value + // ^ this runs a parcel through beforeChange immediately + // shoving outerParcel in as a fake previous value }; // @@ -138,6 +138,12 @@ export default (params: Params): Return => { const handleChange = (newParcel: Parcel, changeRequest: ChangeRequest) => { const {debounce, buffer = true, keepValue} = params; + // remember the origin of the last change + // useParcelBufferInternalKeepValue needs it + newParcel._frameMeta = { + lastOriginId: changeRequest.originId + }; + // remove buffer actions meta from change request // and push any remaining change into the buffer let actions = changeRequest._actions.filter(removeInternalMeta); @@ -166,18 +172,26 @@ export default (params: Params): Return => { }; const newOuterParcel = params.parcel; + setOuterParcel(newOuterParcel); + + // clear buffer if it exists and if we aren't rebasing + if(internalBuffer.bufferState && newOuterParcel._frameMeta.mergeMode !== "rebase") { + internalBuffer.reset(); + } // boundary split to ensure that inner parcels chain are // completely isolated from outer parcels chain newInnerParcel = params.parcel ._boundarySplit({handleChange}) - .pipe( - prepareInnerParcelFromOuter(), - applyModifiers - ); + .pipe(prepareInnerParcelFromOuter()) + ._changeAndReturn(parcel => { + // apply buffered changes to new parcel from props + let changeRequest = internalBuffer.bufferState; + changeRequest && parcel.dispatch(changeRequest); + })[0] + .pipe(applyModifiers); setInnerParcel(newInnerParcel); - setOuterParcel(newOuterParcel); } let returnedParcel: Parcel = innerParcel || newInnerParcel; diff --git a/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js b/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js index 3fabd104..a27d366f 100644 --- a/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js +++ b/packages/react-dataparcels/src/useParcelBufferInternalKeepValue.js @@ -20,8 +20,8 @@ export default ({keepValue, parcel}: Params): boolean => { return false; } - - let changedBySelf = parcel._lastOriginId.startsWith(parcel.id); + let {lastOriginId = ''} = parcel._frameMeta; + let changedBySelf = lastOriginId.startsWith(parcel.id); if(changedBySelf) { keepValueReceivedRef.current = parcel.value; return true; diff --git a/packages/react-dataparcels/src/useParcelForm.js b/packages/react-dataparcels/src/useParcelForm.js index 5ebba6fe..24ff2809 100644 --- a/packages/react-dataparcels/src/useParcelForm.js +++ b/packages/react-dataparcels/src/useParcelForm.js @@ -14,8 +14,8 @@ import useParcelBuffer from './useParcelBuffer'; type Params = { value: any, updateValue?: boolean, - onChange?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise, - onChangeUseResult?: boolean, + onSubmit?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise, + onSubmitUseResult?: boolean, buffer?: boolean, debounce?: number, validation?: ParcelValueUpdater|() => ParcelValueUpdater, @@ -29,8 +29,8 @@ export default (params: Params): Return => { let { value, updateValue = false, - onChange, - onChangeUseResult = false, + onSubmit, + onSubmitUseResult = false, buffer = true, debounce = 0, validation, @@ -54,8 +54,8 @@ export default (params: Params): Return => { let [sideEffectParcel, sideEffectControl] = useParcelSideEffect({ parcel: outerParcel, - onChange, - onChangeUseResult + onSubmit, + onSubmitUseResult }); let [innerParcel, innerParcelControl] = useParcelBuffer({ diff --git a/packages/react-dataparcels/src/useParcelSideEffect.js b/packages/react-dataparcels/src/useParcelSideEffect.js index 787a791f..2b44a5b5 100644 --- a/packages/react-dataparcels/src/useParcelSideEffect.js +++ b/packages/react-dataparcels/src/useParcelSideEffect.js @@ -30,8 +30,8 @@ const mergeQueue = (parcel: Parcel, queue: QueueItem[]): QueueItem[] => { type Params = { parcel: Parcel, - onChange?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise, - onChangeUseResult?: boolean + onSubmit?: (parcel: Parcel, changeRequest: ChangeRequest) => any|Promise, + onSubmitUseResult?: boolean }; type Return = [Parcel, {[key: string]: any}]; @@ -56,10 +56,10 @@ export default (params: Params): Return => { const errorRef = useRef(undefined); // - // onChange status + // submit status // - let getOnChangeStatus = () => { + let getSubmitStatus = () => { let status = statusRef.current; let error = status === 'rejected' ? errorRef.current.error : undefined; @@ -73,9 +73,9 @@ export default (params: Params): Return => { }; // control contains the hooks control object - const [onChangeStatus, setOnChangeStatus] = useState(getOnChangeStatus); + const [submitStatus, setSubmitStatus] = useState(getSubmitStatus); - let updateOnChangeStatus = () => setOnChangeStatus(getOnChangeStatus()); + let updateSubmitStatus = () => setSubmitStatus(getSubmitStatus()); // // queue processing @@ -88,12 +88,12 @@ export default (params: Params): Return => { let [newParcel, changeRequest] = queueRef.current.shift(); onDispatch(newParcel, changeRequest, result); } - updateOnChangeStatus(); + updateSubmitStatus(); }; const processChangeSuccess = processChangeDone((newParcel: Parcel, changeRequest: ChangeRequest, result: any) => { - if(params.onChange && params.onChangeUseResult) { + if(params.onSubmit && params.onSubmitUseResult) { let [/*parcel*/, changeRequestWithResult] = newParcel._changeAndReturn( newParcel => newParcel.set(result) ); @@ -114,17 +114,17 @@ export default (params: Params): Return => { }); const processChange = () => { - let {onChange} = params; + let {onSubmit} = params; - // if no onChange is present, success! skip to the end - if(!onChange) { + // if no onSubmit is present, success! skip to the end + if(!onSubmit) { processChangeSuccess(); return; } // merge remaining changes in the queue together queueRef.current = mergeQueue(params.parcel, queueRef.current); - let result = onChange(...queueRef.current[0]); + let result = onSubmit(...queueRef.current[0]); // if a promise isn't returned, success! skip to the end if(!isPromise(result)) { @@ -133,7 +133,7 @@ export default (params: Params): Return => { } statusRef.current = 'pending'; - updateOnChangeStatus(); + updateSubmitStatus(); // $FlowFixMe - flow can't tell that isPromise() guarantees // that this is a promise @@ -152,7 +152,7 @@ export default (params: Params): Return => { // all changes go into the queue queueRef.current.push([newParcel, changeRequest]); - // if nothing is pending, then call onChange + // if nothing is pending, then call onSubmit if(statusRef.current !== 'pending') { processChange(); } @@ -170,7 +170,7 @@ export default (params: Params): Return => { // let control = { - onChangeStatus + submitStatus }; return [returnedParcel, control]; diff --git a/packages/react-dataparcels/src/useParcelState.js b/packages/react-dataparcels/src/useParcelState.js index fae847b5..e45d5c5d 100644 --- a/packages/react-dataparcels/src/useParcelState.js +++ b/packages/react-dataparcels/src/useParcelState.js @@ -54,6 +54,13 @@ export default (params: Params): Return => { const [parcel, setParcel] = useState(() => updateParcelValue( new Parcel({ handleChange: (parcel: Parcel, changeRequest: ChangeRequest) => { + + // remember the origin of the last change + // useParcelBufferInternalKeepValue needs it + parcel._frameMeta = { + lastOriginId: changeRequest.originId + }; + setParcel(applyBeforeChange(parcel)); if(params.onChange) {