Skip to content

Commit def2a75

Browse files
Merge pull request #77 from dxinteractive/feature/readonly
feat: add readonly forms
2 parents 3655da3 + 4a7e28e commit def2a75

File tree

7 files changed

+84
-15
lines changed

7 files changed

+84
-15
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ npm install --save dendriform
141141
- [Rendering](#rendering)
142142
- [Rendering arrays](#rendering-arrays)
143143
- [Setting data](#setting-data)
144+
- [Readonly forms](#readonly-forms)
144145
- [Updating from props](#updating-from-props)
145146
- [ES6 classes](#es6-classes)
146147
- [ES6 maps](#es6-maps)
@@ -579,6 +580,23 @@ form.done();
579580
// form.value will update to become 3
580581
```
581582
583+
### Readonly forms
584+
585+
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.
586+
587+
```js
588+
const form = new Dendriform(0);
589+
const readonlyForm = form.readonly();
590+
591+
// readonlyForm can have its .value and .useValue read
592+
// can subscribe to changes with .onChange() etc. and can render,
593+
// but calling .set(), .go() or any derivatives
594+
// will cause an error to be thrown
595+
596+
readonlyForm.set(1); // throws error
597+
598+
```
599+
582600
### Updating from props
583601
584602
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.

packages/dendriform/.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module.exports = [
99
name: 'Dendriform',
1010
path: "dist/dendriform.esm.js",
1111
import: "{ Dendriform }",
12-
limit: "9.0 KB",
12+
limit: "9.1 KB",
1313
ignore: ['react', 'react-dom']
1414
}
1515
];

packages/dendriform/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ npm install --save dendriform
141141
- [Rendering](#rendering)
142142
- [Rendering arrays](#rendering-arrays)
143143
- [Setting data](#setting-data)
144+
- [Readonly forms](#readonly-forms)
144145
- [Updating from props](#updating-from-props)
145146
- [ES6 classes](#es6-classes)
146147
- [ES6 maps](#es6-maps)
@@ -579,6 +580,23 @@ form.done();
579580
// form.value will update to become 3
580581
```
581582
583+
### Readonly forms
584+
585+
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.
586+
587+
```js
588+
const form = new Dendriform(0);
589+
const readonlyForm = form.readonly();
590+
591+
// readonlyForm can have its .value and .useValue read
592+
// can subscribe to changes with .onChange() etc. and can render,
593+
// but calling .set(), .go() or any derivatives
594+
// will cause an error to be thrown
595+
596+
readonlyForm.set(1); // throws error
597+
598+
```
599+
582600
### Updating from props
583601
584602
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.

packages/dendriform/src/Dendriform.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,15 @@ export class Core<C,P extends Plugins> {
290290
history: (_id) => this.state.historyState
291291
};
292292

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

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

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

311311
const id = node ? node.id : 'notfound';
312-
return this.getFormById(id);
312+
return this.getFormById(id, readonly);
313313
};
314314

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

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

759761
core: Core<unknown,P>;
760762
id: string;
763+
_readonly = false;
761764

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

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

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

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

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

872-
undo = (): void => this.core.go(-1);
877+
undo = (): void => this.go(-1);
873878

874-
redo = (): void => this.core.go(1);
879+
redo = (): void => this.go(1);
875880

876-
go = (offset: number): void => this.core.go(offset);
881+
go = (offset: number): void => {
882+
if(this._readonly) die(9);
883+
this.core.go(offset);
884+
};
877885

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

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

939947
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>[];
@@ -990,6 +998,10 @@ export class Dendriform<V,P extends Plugins = undefined> {
990998

991999
return <Branch key={form.id} renderer={containerRenderer} deps={deps} />;
9921000
}
1001+
1002+
readonly(): Dendriform<V,P> {
1003+
return this.core.getFormById(this.id, true) as Dendriform<V,P>;
1004+
}
9931005
}
9941006

9951007
//

packages/dendriform/src/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const errors = {
99
5: `sync() forms must have the same maximum number of history items configured`,
1010
6: (msg: string) => `onDerive() callback must not throw errors on first call. Threw: ${msg}`,
1111
7: `Cannot call .set() on an element of an es6 Set`,
12-
8: `Plugin must be passed into a Dendriform instance before this operation can be called`
12+
8: `Plugin must be passed into a Dendriform instance before this operation can be called`,
13+
9: `Cannot call .set() or .go() on a readonly form`
1314
} as const;
1415

1516
export type ErrorKey = keyof typeof errors;

packages/dendriform/src/plugins/PluginSubmit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ export class PluginSubmit<V,E=undefined> extends Plugin {
101101

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

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

112112
get submitting(): Dendriform<boolean> {

packages/dendriform/test/Dendriform.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3254,4 +3254,24 @@ describe(`Dendriform`, () => {
32543254
});
32553255
});
32563256
});
3257+
3258+
describe(`readonly`, () => {
3259+
3260+
test(`should create readonly form`, () => {
3261+
const form = new Dendriform(123);
3262+
3263+
expect(() => form.readonly().set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
3264+
expect(() => form.readonly().undo()).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
3265+
});
3266+
3267+
test(`should create readonly forms branched from a readonly form`, () => {
3268+
const form = new Dendriform({foo: 123});
3269+
3270+
expect(() => form.readonly().branch('foo').set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
3271+
expect(() => form.branch('foo').set(456)).not.toThrow();
3272+
expect(() => form.readonly().branch('foo').set(456)).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
3273+
// @ts-ignore
3274+
expect(() => form.readonly().branch('foo').setParent({foo: 456})).toThrow(`[Dendriform] Cannot call .set() or .go() on a readonly form`);
3275+
});
3276+
});
32573277
});

0 commit comments

Comments
 (0)