Skip to content

Commit

Permalink
Merge pull request #82 from dxinteractive/feature/permissive-branching
Browse files Browse the repository at this point in the history
Make branchAll() and renderAll() on non-iterable types consistent with branch() and render()
  • Loading branch information
dxinteractive authored Feb 28, 2022
2 parents b0c7241 + 8e7c4e3 commit 54cf3e9
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 28 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ npm install --save dendriform
- [Creation](#creation)
- [Values](#values)
- [Branching](#branching)
- [Branching multiple children](#branching-multiple-children)
- [Rendering](#rendering)
- [Rendering arrays](#rendering-arrays)
- [Rendering arrays and multiple children](#rendering-arrays-and-multiple-children)
- [Setting data](#setting-data)
- [Read-only forms](#readonly-forms)
- [Updating from props](#updating-from-props)
Expand Down Expand Up @@ -274,15 +275,29 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#branch)
A form containing a non-branchable value such as a string, number, undefined or null will throw an error if `.branch()` is called on it. You can check if a form is branchable using `.branchable`:
You can check if a form is branchable using `.branchable`. On a form containing a non-branchable value such as a string, number, undefined or null it will return false, or if the form is branchable it will return true.
```js
new Dendriform(123).branchable; // returns false
new Dendriform({name: 'Bill'}).branchable; // returns true
```
You can still call `.branch()` on non-branchable forms - the returned form will be read-only and contain a value of undefined. While this may seem overly loose, it is to prevent the proliferation of safe-guarding code in userland, and is useful for situations where React components that render branched forms are still briefly mounted after a parent values changes from a branchable type to a non-branchable type.
### Branching multiple children
The `.branchAll()` methods can be used to branch all children at once, returning an array of branched forms.
```js
const form = new Dendriform(['a','b','c']);

const elementForms = form.branchAll();
// elementForms.length is 3
// elementForms[0].value is 'a'
```
You can still call `.branchAll()` on non-branchable or non-iterable forms - it will return an empty array in this case.
### Rendering
The `.render()` function allows you to branch off and render a deep value in a React component.
Expand Down Expand Up @@ -404,7 +419,9 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#renderdeps)
### Rendering arrays
You can still call `.render()` on non-branchable forms - the returned form will be read-only and contain a value of undefined. While this may seem overly loose, it is to prevent the proliferation of safe-guarding code in userland, and is useful for situations where React components that render branched forms are still briefly mounted after a parent values changes from a branchable type to a non-branchable type.
### Rendering arrays and multiple children
The `.renderAll()` function works in the same way as `.render()`, but repeats for all elements in an array. React keying is taken care of for you.
Expand Down Expand Up @@ -463,6 +480,8 @@ const petName = form.branch(['pets', 0, 'name']);
Like with `.render()`, the `.renderAll()` function can also additionally accept an array of dependencies that will cause it to update in response to prop changes.
You can still call `.renderAll()` on non-branchable or non-iterable forms - it will return an empty array in this case.
### Setting data
You can set data directly using `.set()`. This accepts the new value for the form. When called, changes will immediately be applied to the data in the form and any relevant `.useValue()` hooks and `.render()` methods will be scheduled to update by React.
Expand Down
25 changes: 22 additions & 3 deletions packages/dendriform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ npm install --save dendriform
- [Creation](#creation)
- [Values](#values)
- [Branching](#branching)
- [Branching multiple children](#branching-multiple-children)
- [Rendering](#rendering)
- [Rendering arrays](#rendering-arrays)
- [Rendering arrays and multiple children](#rendering-arrays-and-multiple-children)
- [Setting data](#setting-data)
- [Read-only forms](#readonly-forms)
- [Updating from props](#updating-from-props)
Expand Down Expand Up @@ -274,15 +275,29 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#branch)
A form containing a non-branchable value such as a string, number, undefined or null will throw an error if `.branch()` is called on it. You can check if a form is branchable using `.branchable`:
You can check if a form is branchable using `.branchable`. On a form containing a non-branchable value such as a string, number, undefined or null it will return false, or if the form is branchable it will return true.
```js
new Dendriform(123).branchable; // returns false
new Dendriform({name: 'Bill'}).branchable; // returns true
```
You can still call `.branch()` on non-branchable forms - the returned form will be read-only and contain a value of undefined. While this may seem overly loose, it is to prevent the proliferation of safe-guarding code in userland, and is useful for situations where React components that render branched forms are still briefly mounted after a parent values changes from a branchable type to a non-branchable type.
### Branching multiple children
The `.branchAll()` methods can be used to branch all children at once, returning an array of branched forms.
```js
const form = new Dendriform(['a','b','c']);

const elementForms = form.branchAll();
// elementForms.length is 3
// elementForms[0].value is 'a'
```
You can still call `.branchAll()` on non-branchable or non-iterable forms - it will return an empty array in this case.
### Rendering
The `.render()` function allows you to branch off and render a deep value in a React component.
Expand Down Expand Up @@ -404,7 +419,9 @@ function MyComponent(props) {
[Demo](http://dendriform.xyz#renderdeps)
### Rendering arrays
You can still call `.render()` on non-branchable forms - the returned form will be read-only and contain a value of undefined. While this may seem overly loose, it is to prevent the proliferation of safe-guarding code in userland, and is useful for situations where React components that render branched forms are still briefly mounted after a parent values changes from a branchable type to a non-branchable type.
### Rendering arrays and multiple children
The `.renderAll()` function works in the same way as `.render()`, but repeats for all elements in an array. React keying is taken care of for you.
Expand Down Expand Up @@ -463,6 +480,8 @@ const petName = form.branch(['pets', 0, 'name']);
Like with `.render()`, the `.renderAll()` function can also additionally accept an array of dependencies that will cause it to update in response to prop changes.
You can still call `.renderAll()` on non-branchable or non-iterable forms - it will return an empty array in this case.
### Setting data
You can set data directly using `.set()`. This accepts the new value for the form. When called, changes will immediately be applied to the data in the form and any relevant `.useValue()` hooks and `.render()` methods will be scheduled to update by React.
Expand Down
15 changes: 8 additions & 7 deletions packages/dendriform/src/Dendriform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import produce, {isDraft, original} from 'immer';
import {producePatches, Patch} from './producePatches';
import type {ToProduce} from './producePatches';
import {die} from './errors';
import type {ErrorKey} from './errors';
import {newNode, addNode, getPath, getNodeByPath, produceNodePatches, getNode} from './Nodes';
import type {Nodes, NodeAny, NewNodeCreator} from './Nodes';
import type {Plugin} from './Plugin';
Expand Down Expand Up @@ -316,8 +315,10 @@ export class Core<C,P extends Plugins> {
});
}

const id = node ? node.id : 'notfound';
return this.getFormById(id, readonly);
if(!node) {
return this.getFormById('notfound', true);
}
return this.getFormById(node.id, readonly);
};

getFormById = (id: string, readonly: boolean): Dendriform<unknown,P> => {
Expand Down Expand Up @@ -714,11 +715,11 @@ const Branch = React.memo(
const branchable = (thing: any) => getType(thing) !== BASIC;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const entriesOrDie = (thing: any, error: ErrorKey) => {
const entriesOrNone = (thing: any): any[] => {
try {
return entries(thing);
} catch(e) {
die(error);
return [];
}
};

Expand Down Expand Up @@ -965,7 +966,7 @@ export class Dendriform<V,P extends Plugins = undefined> {
branchAll(pathOrKey: any): any {
const got = this.branch(pathOrKey);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return entriesOrDie(got.value, 2).map(([key]) => got.branch(key as any));
return entriesOrNone(got.value).map(([key]) => got.branch(key as any));
}

render<K1 extends Key<V>, K2 extends keyof Val<V,K1>, K3 extends keyof Val<Val<V,K1>,K2>, K4 extends keyof Val<Val<Val<V,K1>,K2>,K3>>(path: [K1, K2, K3, K4], renderer: Renderer<Dendriform<Val<Val<Val<V,K1>,K2>,K3>[K4],P>>, deps?: unknown[]): React.ReactElement;
Expand Down Expand Up @@ -1000,7 +1001,7 @@ export class Dendriform<V,P extends Plugins = undefined> {

const containerRenderer = (): React.ReactElement[] => {
const value = form.useValue();
return entriesOrDie(value, 3).map(([key]): React.ReactElement => {
return entriesOrNone(value).map(([key]): React.ReactElement => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child = form.branch(key as any);
return <Branch key={child.id} renderer={() => renderer(child)} deps={deps} />;
Expand Down
6 changes: 3 additions & 3 deletions packages/dendriform/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const all = `can only be called on forms containing an array, object, es6 map or es6 set`;
// const all = `can only be called on forms containing an array, object, es6 map or es6 set`;

const errors = {
0: (id: number) => `Cannot find path of node ${id}`,
// 1: (path: unknown[]) => `Cannot find node at path ${path.map(a => JSON.stringify(a)).join('","')}`,
2: `branchAll() ${all}`,
3: `renderAll() ${all}`,
// 2: `branchAll() ${all}`,
// 3: `renderAll() ${all}`,
4: (path: unknown[]) => `useIndex() can only be called on array element forms, can't be called at path ${path.map(a => JSON.stringify(a)).join('","')}`,
5: `sync() forms must have the same maximum number of history items configured`,
6: (msg: string) => `onDerive() callback must not throw errors on first call. Threw: ${msg}`,
Expand Down
17 changes: 8 additions & 9 deletions packages/dendriform/test/Dendriform.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ describe(`Dendriform`, () => {

expect(barForm.value).toBe(undefined);
expect(barForm.id).toBe('notfound');
expect(barForm._readonly).toBe(true);
});

test(`should get deleted child value`, () => {
Expand All @@ -803,6 +804,7 @@ describe(`Dendriform`, () => {
form.set([[[123]]]);

expect(elemForm.branch(0).value).toBe(undefined);
expect(elemForm.branch(0)._readonly).toBe(true);
expect(form.branch([0,0,0]).value).toBe(123);
});
});
Expand Down Expand Up @@ -885,10 +887,10 @@ describe(`Dendriform`, () => {
expect(forms.map(f => f.value)).toEqual([0,1]);
});

test(`should error if getting a basic type`, () => {
test(`should NOT error if getting a basic type`, () => {
const form = new Dendriform(123);

expect(() => form.branchAll()).toThrow('branchAll() can only be called on forms containing an array, object, es6 map or es6 set');
expect(form.branchAll()).toEqual([]);
});

// TODO what about misses?
Expand Down Expand Up @@ -1174,11 +1176,7 @@ describe(`Dendriform`, () => {

describe(`rendering`, () => {

test(`should error if rendering a basic type`, () => {
const consoleError = console.error;
// eslint-disable-next-line @typescript-eslint/no-empty-function
console.error = () => {};

test(`should NOT error if rendering a basic type`, () => {
const form = new Dendriform('4');

const renderer = jest.fn(form => <div className="branch">{form.value}</div>);
Expand All @@ -1187,9 +1185,10 @@ describe(`Dendriform`, () => {
return props.form.renderAll(renderer);
};

expect(() => mount(<MyComponent form={form} foo={1} />)).toThrow('renderAll() can only be called on forms containing an array, object, es6 map or es6 set');
const wrapper = mount(<MyComponent form={form} foo={1} />);

console.error = consoleError;
expect(renderer).toHaveBeenCalledTimes(0);
expect(wrapper.find('.branch').length).toBe(0);
});

test(`should renderAll no levels and return React element`, () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/dendriform/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ describe(`die`, () => {
test(`should throw errors`, () => {
expect(() => die(0, 123)).toThrow(`[Dendriform] Cannot find path of node 123`);
// expect(() => die(1, ['a',1])).toThrow(`[Dendriform] Cannot find node at path ["a",1]`);
expect(() => die(2)).toThrow(`[Dendriform] branchAll() can only be called on forms containing an array, object, es6 map or es6 set`);
expect(() => die(3)).toThrow(`[Dendriform] renderAll() can only be called on forms containing an array, object, es6 map or es6 set`);
// expect(() => die(2)).toThrow(`[Dendriform] branchAll() can only be called on forms containing an array, object, es6 map or es6 set`);
// expect(() => die(3)).toThrow(`[Dendriform] renderAll() can only be called on forms containing an array, object, es6 map or es6 set`);
expect(() => die(4, ['foo'])).toThrow(`[Dendriform] useIndex() can only be called on array element forms, can't be called at path [\"foo\"]`);
});

Expand All @@ -19,7 +19,7 @@ describe(`die (prod mode)`, () => {
test(`should throw minified errors`, () => {
global.__DEV__ = false;
expect(() => die(0, 123, "woo")).toThrow(`[Dendriform] minified error #0: 123, "woo"`);
expect(() => die(2)).toThrow(`[Dendriform] minified error #2:`);
expect(() => die(4)).toThrow(`[Dendriform] minified error #4:`);
});
});

0 comments on commit 54cf3e9

Please sign in to comment.