Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix length-based resize, add set() debounce, add useProps() #48

Merged
merged 6 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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