Skip to content

Commit

Permalink
Merge pull request #48 from 92green/feature/fix-length-resize
Browse files Browse the repository at this point in the history
Fix length-based resize, add set() debounce, add useProps()
  • Loading branch information
dxinteractive authored Jul 21, 2021
2 parents 4f1471e + 3577d82 commit 0af2a78
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 41 deletions.
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ function MyComponent(props) {

return <div>
{form.render('count', form => {
const [count] = form.useValue();
const count = form.useValue();
return <div>Count: {count}</div>;
})}

Expand All @@ -499,6 +499,27 @@ function MyComponent(props) {
}
```
The `.set()` function can also be debounced by passing a number of milliseconds as the second argument to `.set()`.
```js
function MyComponent(props) {
const form = useDendriform(0);

const countUpDebounced = useCallback(() => {
form.set(count => count + 1, 100);
}, []);

return <div>
{form.render(form => {
const count = form.useValue();
return <div>Count: {count}</div>;
})}

<button onClick={countUpDebounced}>Count up</button>
</div>;
}
```
#### Buffering
To call it multiple times in a row, use `buffer()` to begin buffering changes and `done()` to apply the changes. These will affect the entire form including all branches, so `form.buffer()` has the same effect as `form.branch('example').buffer()`.
Expand Down Expand Up @@ -671,6 +692,28 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#inputs)
You may also have form input components of your own whose `onChange` functions are called with the new value rather than a change event. The `useProps` hook can be spread onto these elements in a similar way to the `useInput` hook. These also support debouncing.
```js
import {useDendriform, useProps} from 'dendriform';

function MyComponent(props) {

const form = useDendriform([]);

return <MySelectComponent {...useProps(form)} />;
};
```
This is equivalent to doing the following:
```js
return <MySelectComponent
value={form.value}
onChange={value => form.set(value)}
/>;
```
### Subscribing to changes
You can subscribe to changes using `.onChange`, or by using the `.useChange()` hook if you're inside a React component's render method.
Expand Down
60 changes: 60 additions & 0 deletions packages/dendriform-demo/components/Demos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,58 @@ function MyComponent(props) {
}
`;

//
// setting data with debouncing
//

function SettingDataDebounce(): React.ReactElement {
const form = useDendriform({
a: 0,
b: 0
});

const changeA = useCallback(() => {
form.branch('a').set(Math.floor(Math.random() * 1000), 300);
}, []);

const changeB = useCallback(() => {
form.branch('b').set(Math.floor(Math.random() * 1000), 300);
}, []);

return <Region>
{form.render('a', form => <Region of="code">{form.useValue()}</Region>)}
{form.render('b', form => <Region of="code">{form.useValue()}</Region>)}

<button type="button" onClick={changeA}>change a with 300ms debounce</button>
<button type="button" onClick={changeB}>change b with 300ms debounce</button>
</Region>;
}

const SettingDataDebounceCode = `
function MyComponent(props) {
const form = useDendriform({
a: 0,
b: 0
});
const changeA = useCallback(() => {
form.branch('a').set(Math.floor(Math.random() * 1000), 300);
}, []);
const changeB = useCallback(() => {
form.branch('b').set(Math.floor(Math.random() * 1000), 300);
}, []);
return <div>
{form.render('a', form => <code>a: {form.useValue()}</code>)}
{form.render('b', form => <code>b: {form.useValue()}</code>)}
<button type="button" onClick={changeA}>change a with 300ms debounce</button>
<button type="button" onClick={changeB}>change b with 300ms debounce</button>
</div>;
}
`;

//
// es6 classes
//
Expand Down Expand Up @@ -1860,6 +1912,14 @@ const DEMOS: DemoObject[] = [
anchor: 'buffer',
more: 'setting-data'
},
{
title: 'Setting data with debouncing',
Demo: SettingDataDebounce,
code: SettingDataDebounceCode,
description: `This demonstrates how set() calls can be debounced. Click the buttons below rapidly and watch how the value updates more slowly.`,
anchor: 'debounce',
more: 'debounce'
},
{
title: 'ES6 classes',
Demo: ES6Classes,
Expand Down
4 changes: 2 additions & 2 deletions packages/dendriform/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = [
{
name: 'everything combined',
path: "dist/dendriform.esm.js",
limit: "8.4 KB",
limit: "8.6 KB",
ignore: ['react', 'react-dom']
},
{
Expand All @@ -16,7 +16,7 @@ module.exports = [
name: 'Dendriform',
path: "dist/dendriform.esm.js",
import: "{ Dendriform }",
limit: "7.6 KB",
limit: "7.8 KB",
ignore: ['react', 'react-dom']
},
{
Expand Down
45 changes: 44 additions & 1 deletion packages/dendriform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ function MyComponent(props) {

return <div>
{form.render('count', form => {
const [count] = form.useValue();
const count = form.useValue();
return <div>Count: {count}</div>;
})}

Expand All @@ -499,6 +499,27 @@ function MyComponent(props) {
}
```
The `.set()` function can also be debounced by passing a number of milliseconds as the second argument to `.set()`.
```js
function MyComponent(props) {
const form = useDendriform(0);

const countUpDebounced = useCallback(() => {
form.set(count => count + 1, 100);
}, []);

return <div>
{form.render(form => {
const count = form.useValue();
return <div>Count: {count}</div>;
})}

<button onClick={countUpDebounced}>Count up</button>
</div>;
}
```
#### Buffering
To call it multiple times in a row, use `buffer()` to begin buffering changes and `done()` to apply the changes. These will affect the entire form including all branches, so `form.buffer()` has the same effect as `form.branch('example').buffer()`.
Expand Down Expand Up @@ -671,6 +692,28 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#inputs)
You may also have form input components of your own whose `onChange` functions are called with the new value rather than a change event. The `useProps` hook can be spread onto these elements in a similar way to the `useInput` hook. These also support debouncing.
```js
import {useDendriform, useProps} from 'dendriform';

function MyComponent(props) {

const form = useDendriform([]);

return <MySelectComponent {...useProps(form)} />;
};
```
This is equivalent to doing the following:
```js
return <MySelectComponent
value={form.value}
onChange={value => form.set(value)}
/>;
```
### Subscribing to changes
You can subscribe to changes using `.onChange`, or by using the `.useChange()` hook if you're inside a React component's render method.
Expand Down
21 changes: 17 additions & 4 deletions packages/dendriform/src/Dendriform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,19 @@ class Core<C> {
// setting data
//

debounceMap = new Map<string,number>();

setWithDebounce = (id: string, toProduce: unknown, debounce = 0): void => {
if(debounce === 0) {
this.set(id, toProduce);
return;
}

const countAtCall = (this.debounceMap.get(id) ?? 0) + 1;
this.debounceMap.set(id, countAtCall);
setTimeout(() => countAtCall === this.debounceMap.get(id) && this.set(id, toProduce), debounce);
};

// if setBuffer exists, then new changes will be merged onto it
// if not, a new change will push a new history item
bufferingChanges = false;
Expand Down Expand Up @@ -518,14 +531,14 @@ export class Dendriform<V,C=V> {
return this.core.historyState;
}

set = (toProduce: ToProduce<V>): void => {
this.core.set(this.id, toProduce);
set = (toProduce: ToProduce<V>, debounce = 0): void => {
this.core.setWithDebounce(this.id, toProduce, debounce);
};

setParent = (childToProduce: ChildToProduce<unknown>): void => {
setParent = (childToProduce: ChildToProduce<unknown>, debounce = 0): void => {
const basePath = this.core.getPathOrError(this.id);
const parent = this.core.getFormAt(basePath.slice(0,-1));
this.core.set(parent.id, childToProduce(basePath[basePath.length - 1]));
this.core.setWithDebounce(parent.id, childToProduce(basePath[basePath.length - 1]), debounce);
};

onChange(callback: ChangeCallback<number>, changeType: ChangeTypeIndex): (() => void);
Expand Down
26 changes: 23 additions & 3 deletions packages/dendriform/src/Nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ export const updateNode = (nodes: Nodes, id: string, value: unknown): void => {
};
};

export const updateArrayNodeLength = (nodes: Nodes, id: string, length: number): void => {
const node = get(nodes, id) as NodeAny|undefined;
if(!node || node.type !== ARRAY) return;

const child = node.child as string[];
const newChild = child.slice(0, length);
child.slice(length).forEach(id => {
removeNode(nodes, id);
});

nodes[id] = {
...node,
child: newChild
};
};

export const produceNodePatches = (
nodes: Nodes,
newNodeCreator: NewNodeCreator,
Expand Down Expand Up @@ -186,6 +202,13 @@ export const produceNodePatches = (
op = 'replace';
}

// if an array is changed by altering length
// change the patch to do the same thing via a replace
if(parentNode.type === ARRAY && path[path.length - 1] === 'length') {
updateArrayNodeLength(draft, parentNode.id, value as number);
return;
}

// depending on type, make changes to the child node
// and to the parent node's child
if(op === 'add') {
Expand Down Expand Up @@ -233,9 +256,6 @@ export const produceNodePatches = (
applyPatches(draft, patchesForNodes);
});


//console.log('node patches', result[1]);

(result[1] as DendriformPatch[]).forEach(patch => patch.namespace = 'nodes');
(result[2] as DendriformPatch[]).forEach(patch => patch.namespace = 'nodes');
return result as readonly [Nodes, DendriformPatch[], DendriformPatch[]];
Expand Down
1 change: 1 addition & 0 deletions packages/dendriform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export * from './Nodes';
export * from './array';
export * from './useInput';
export * from './useCheckbox';
export * from './useProps';
export * from './sync';
export {immerable} from 'immer';
2 changes: 1 addition & 1 deletion packages/dendriform/src/sync.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {noChange} from './index';
import type {Dendriform, DeriveCallback, DeriveCallbackDetails} from './index';
import {noChange} from './producePatches';
import {die} from './errors';
import {useEffect} from 'react';

Expand Down
18 changes: 2 additions & 16 deletions packages/dendriform/src/useInput.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import {useState, useCallback, useRef} from 'react';
import {useState, useCallback} from 'react';
import type {Dendriform} from './Dendriform';

type DebouncedCallbackReturn<A> = (arg: A) => void;

export const useDebounceCallback = <A,>(debounce: number, callback: (arg: A) => void, deps: unknown[]): DebouncedCallbackReturn<A> => {
const count = useRef(0);
return useCallback(arg => {
const countAtCall = ++count.current;
setTimeout(() => countAtCall === count.current && callback(arg), debounce);
}, deps);
};

type UseInputResult = {
value: string,
onChange: (event: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement>) => void
Expand All @@ -26,14 +16,10 @@ export const useInput = <V extends string|null|undefined,C>(form: Dendriform<V,C
setLocalValue(formValue);
}

const onChangeDebounced = useDebounceCallback(debounce, (value: V) => {
form.set(value);
}, []);

const onChange = useCallback(event => {
const newValue = event.target.value;
setLocalValue(newValue);
onChangeDebounced(newValue);
form.set(newValue, debounce);
}, []);

return {
Expand Down
28 changes: 28 additions & 0 deletions packages/dendriform/src/useProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {useState, useCallback} from 'react';
import type {Dendriform} from './Dendriform';

type UsePropsResult<V> = {
value: V,
onChange: (newValue: V) => void
};

export const useProps = <V,C>(form: Dendriform<V,C>, debounce = 0): UsePropsResult<V> => {
const formValue = form.useValue();
const [lastFormValue, setLastFormValue] = useState(formValue);
const [localValue, setLocalValue] = useState(formValue);

if(formValue !== lastFormValue) {
setLastFormValue(formValue);
setLocalValue(formValue);
}

const onChange = useCallback((newValue: V) => {
setLocalValue(newValue);
form.set(newValue, debounce);
}, []);

return {
value: localValue,
onChange
};
};
Loading

0 comments on commit 0af2a78

Please sign in to comment.