Skip to content

Commit

Permalink
Merge pull request #77 from dxinteractive/feature/readonly
Browse files Browse the repository at this point in the history
feat: add readonly forms
  • Loading branch information
dxinteractive authored Feb 27, 2022
2 parents 3655da3 + 4a7e28e commit def2a75
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 15 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ npm install --save dendriform
- [Rendering](#rendering)
- [Rendering arrays](#rendering-arrays)
- [Setting data](#setting-data)
- [Readonly forms](#readonly-forms)
- [Updating from props](#updating-from-props)
- [ES6 classes](#es6-classes)
- [ES6 maps](#es6-maps)
Expand Down Expand Up @@ -579,6 +580,23 @@ form.done();
// form.value will update to become 3
```
### Readonly forms
You may want to allow subscribers to a form, while also preventing them from making any changes. For this use case, the `readonly()` method returns a version of the form that cannot be set and cannot navigate history. Any forms branched off a readonly form will also be unable to set or navigate history.
```js
const form = new Dendriform(0);
const readonlyForm = form.readonly();

// readonlyForm can have its .value and .useValue read
// can subscribe to changes with .onChange() etc. and can render,
// but calling .set(), .go() or any derivatives
// will cause an error to be thrown

readonlyForm.set(1); // throws error

```
### Updating from props
The `useDendriform` hook can automatically update when props change. If a `dependencies` array is passed as an option, the dependencies are checked using `Object.is()` equality to determine if the form should update. If an update is required, the `value` function is called again and the form is set to the result.
Expand Down
2 changes: 1 addition & 1 deletion packages/dendriform/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = [
name: 'Dendriform',
path: "dist/dendriform.esm.js",
import: "{ Dendriform }",
limit: "9.0 KB",
limit: "9.1 KB",
ignore: ['react', 'react-dom']
}
];
18 changes: 18 additions & 0 deletions packages/dendriform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ npm install --save dendriform
- [Rendering](#rendering)
- [Rendering arrays](#rendering-arrays)
- [Setting data](#setting-data)
- [Readonly forms](#readonly-forms)
- [Updating from props](#updating-from-props)
- [ES6 classes](#es6-classes)
- [ES6 maps](#es6-maps)
Expand Down Expand Up @@ -579,6 +580,23 @@ form.done();
// form.value will update to become 3
```
### Readonly forms
You may want to allow subscribers to a form, while also preventing them from making any changes. For this use case, the `readonly()` method returns a version of the form that cannot be set and cannot navigate history. Any forms branched off a readonly form will also be unable to set or navigate history.
```js
const form = new Dendriform(0);
const readonlyForm = form.readonly();

// readonlyForm can have its .value and .useValue read
// can subscribe to changes with .onChange() etc. and can render,
// but calling .set(), .go() or any derivatives
// will cause an error to be thrown

readonlyForm.set(1); // throws error

```
### Updating from props
The `useDendriform` hook can automatically update when props change. If a `dependencies` array is passed as an option, the dependencies are checked using `Object.is()` equality to determine if the form should update. If an update is required, the `value` function is called again and the form is set to the result.
Expand Down
34 changes: 23 additions & 11 deletions packages/dendriform/src/Dendriform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,15 @@ export class Core<C,P extends Plugins> {
history: (_id) => this.state.historyState
};

createForm = (id: string): Dendriform<unknown,P> => {
createForm = (id: string, readonly: boolean): Dendriform<unknown,P> => {
const __branch = {core: this, id};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const form = new Dendriform<any,P>({__branch});
this.dendriforms.set(id, form);
this.dendriforms.set(`${readonly ? 'r' : 'w'}${id}`, form);
return form;
};

getFormAt = (path: Path|undefined): Dendriform<unknown,P> => {
getFormAt = (path: Path|undefined, readonly: boolean): Dendriform<unknown,P> => {
let node: NodeAny|undefined;

if(path) {
Expand All @@ -309,11 +309,13 @@ export class Core<C,P extends Plugins> {
}

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

getFormById = (id: string): Dendriform<unknown,P> => {
return this.dendriforms.get(id) || this.createForm(id);
getFormById = (id: string, readonly: boolean): Dendriform<unknown,P> => {
const form = this.dendriforms.get(`${readonly ? 'r' : 'w'}${id}`) || this.createForm(id, readonly);
form._readonly = readonly;
return form;
};

//
Expand Down Expand Up @@ -758,6 +760,7 @@ export class Dendriform<V,P extends Plugins = undefined> {

core: Core<unknown,P>;
id: string;
_readonly = false;

constructor(initialValue: V|DendriformBranch<P>, options: Options<P> = {}) {

Expand Down Expand Up @@ -821,12 +824,14 @@ export class Dendriform<V,P extends Plugins = undefined> {
}

set = (toProduce: ToProduce<V>, options: SetOptions = {}): void => {
if(this._readonly) die(9);
this.core.setWithDebounce(this.id, toProduce, options);
};

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

Expand Down Expand Up @@ -869,11 +874,14 @@ export class Dendriform<V,P extends Plugins = undefined> {
return () => void this.core.changeCallbackRefs.delete(changeCallback);
}

undo = (): void => this.core.go(-1);
undo = (): void => this.go(-1);

redo = (): void => this.core.go(1);
redo = (): void => this.go(1);

go = (offset: number): void => this.core.go(offset);
go = (offset: number): void => {
if(this._readonly) die(9);
this.core.go(offset);
};

replace = (replace = true): void => this.core.replace(replace);

Expand Down Expand Up @@ -933,7 +941,7 @@ export class Dendriform<V,P extends Plugins = undefined> {
branch(pathOrKey: any): any {
const appendPath = ([] as Path).concat(pathOrKey ?? []);
const basePath = this.core.getPath(this.id);
return this.core.getFormAt(basePath?.concat(appendPath));
return this.core.getFormAt(basePath?.concat(appendPath), this._readonly);
}

branchAll<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>, W extends Val<Val<Val<V,K1>,K2>,K3>[K4]>(path: [K1, K2, K3, K4]): Dendriform<BranchableChild<W>,P>[];
Expand Down Expand Up @@ -990,6 +998,10 @@ export class Dendriform<V,P extends Plugins = undefined> {

return <Branch key={form.id} renderer={containerRenderer} deps={deps} />;
}

readonly(): Dendriform<V,P> {
return this.core.getFormById(this.id, true) as Dendriform<V,P>;
}
}

//
Expand Down
3 changes: 2 additions & 1 deletion packages/dendriform/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const errors = {
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}`,
7: `Cannot call .set() on an element of an es6 Set`,
8: `Plugin must be passed into a Dendriform instance before this operation can be called`
8: `Plugin must be passed into a Dendriform instance before this operation can be called`,
9: `Cannot call .set() or .go() on a readonly form`
} as const;

export type ErrorKey = keyof typeof errors;
Expand Down
4 changes: 2 additions & 2 deletions packages/dendriform/src/plugins/PluginSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ export class PluginSubmit<V,E=undefined> extends Plugin {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private getForm(): Dendriform<any> {
return this.getState().form.core.getFormAt(this.path);
return this.getState().form.core.getFormAt(this.path, true);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
get previous(): Dendriform<any> {
return this.getState().previous.core.getFormAt(this.path);
return this.getState().previous.core.getFormAt(this.path, true);
}

get submitting(): Dendriform<boolean> {
Expand Down
20 changes: 20 additions & 0 deletions packages/dendriform/test/Dendriform.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3254,4 +3254,24 @@ describe(`Dendriform`, () => {
});
});
});

describe(`readonly`, () => {

test(`should create readonly form`, () => {
const form = new Dendriform(123);

expect(() => form.readonly().set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
expect(() => form.readonly().undo()).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
});

test(`should create readonly forms branched from a readonly form`, () => {
const form = new Dendriform({foo: 123});

expect(() => form.readonly().branch('foo').set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
expect(() => form.branch('foo').set(456)).not.toThrow();
expect(() => form.readonly().branch('foo').set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
// @ts-ignore
expect(() => form.readonly().branch('foo').setParent({foo: 456})).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
});
});
});

0 comments on commit def2a75

Please sign in to comment.