8000 add support for readonly · angular/angular@637715f · GitHub
[go: up one dir, main page]

Skip to content

Commit 637715f

Browse files
committed
add support for readonly
1 parent 383543e commit 637715f

File tree

7 files changed

+86
-8
lines changed

7 files changed

+86
-8
lines changed

packages/forms/experimental/src/api/control.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface FormUiControl<TValue> {
1313
readonly value: ModelSignal<TValue>;
1414
readonly errors?: InputSignal<readonly FormError[] | undefined>;
1515
readonly disabled?: InputSignal<boolean | string | undefined>;
16+
readonly readonly?: InputSignal<boolean | undefined>;
1617
readonly valid?: InputSignal<boolean | undefined>;
1718
readonly touched?: InputSignal<boolean | undefined>;
1819

packages/forms/experimental/src/api/logic.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {MetadataKey, DISABLED_REASON, REQUIRED} from '../api/metadata';
9+
import {DISABLED_REASON, MetadataKey, REQUIRED} from '../api/metadata';
1010
import {FieldPathNode} from '../path_node';
1111
import {assertPathIsCurrent} from '../schema';
12-
import type {FieldPath, FormError, LogicFn, TreeValidator, Validator} from './types';
12+
import type {FieldPath, LogicFn, TreeValidator, Validator} from './types';
1313

1414
/**
1515
* Adds logic to a field to conditionally disable it.
@@ -21,7 +21,7 @@ import type {FieldPath, FormError, LogicFn, TreeValidator, Validator} from './ty
2121
*/
2222
export function disabled<T>(
2323
path: FieldPath<T>,
24-
logic: NoInfer<LogicFn<T, boolean | string>>,
24+
logic: NoInfer<LogicFn<T, boolean | string>> = () => true,
2525
): void {
2626
assertPathIsCurrent(path);
2727

@@ -39,6 +39,20 @@ export function disabled<T>(
3939
metadata(path, DISABLED_REASON, reasonFn);
4040
}
4141

42+
/**
43+
* Adds logic to a field to conditionally make it readonly.
44+
*
45+
* @param path The target path to make readonly.
46+
* @param logic A `LogicFn<T, boolean>` that returns `true` when the field is readonly.
47+
* @template T The data type of the field the logic is being added to.
48+
*/
49+
export function readonly<T>(path: FieldPath<T>, logic: NoInfer<LogicFn<T, boolean>> = () => true) {
50+
assertPathIsCurrent(path);
51+
52+
const pathNode = FieldPathNode.unwrapFieldPath(path);
53+
pathNode.logic.readonly.push(logic);
54+
}
55+
4256
/**
4357
* Adds logic to a field to conditionally hide it. A hidden field does not contribute to the
4458
* validation, touched/dirty, or other state of its parent field.

packages/forms/experimental/src/api/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {Signal, WritableSignal} from '@angular/core';
10-
import {MetadataKey} from './metadata';
1110
import {DataKey} from './data';
11+
import {MetadataKey} from './metadata';
1212

1313
/**
1414
* Symbol used to retain generic type information when it would otherwise be lost.
@@ -86,6 +86,10 @@ export interface FieldState<T> {
8686
* A signal indicating whether the field is currently disabled.
8787
*/
8888
readonly disabled: Signal<boolean>;
89+
/**
90+
* A signal indicating whether the field is currently readonly.
91+
*/
92+
readonly readonly: Signal<boolean>;
8993
/**
9094
* A signal containing the current errors for the field.
9195
*/

packages/forms/experimental/src/controls/field.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export class FieldDirective<T> {
100100
// Input bindings:
101101
maybeSynchronize(injector, () => this.field().$state.value(), cmp.value);
102102
maybeSynchronize(injector, () => this.field().$state.disabled(), cmp.disabled);
103+
maybeSynchronize(injector, () => this.field().$state.readonly(), cmp.readonly);
103104
maybeSynchronize(injector, () => this.field().$state.errors(), cmp.errors);
104105
maybeSynchronize(injector, () => this.field().$state.touched(), cmp.touched);
105106
maybeSynchronize(injector, () => this.field().$state.valid(), cmp.valid);

packages/forms/experimental/src/field_node.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
untracked,
1818
WritableSignal,
1919
} from '@angular/core';
20+
import {DataKey} from './api/data';
2021
import {MetadataKey} from './api/metadata';
2122
import type {
2223
Field,
@@ -31,7 +32,6 @@ import type {
3132
import {DYNAMIC, FieldLogicNode} from './logic_node';
3233
import {FieldPathNode, FieldRootPathNode} from './path_node';
3334
import {deepSignal} from './util/deep_signal';
34-
import {DataKey} from './api/data';
3535

3636
export interface DataEntry {
3737
value: unknown;
@@ -284,6 +284,16 @@ export class FieldNode implements FieldState<unknown> {
284284
() => (this.parent?.disabled() || this.logic.disabled.compute(this.fieldContext)) ?? false,
285285
);
286286

287+
/**
288+
* Whether this field is considered readonly.
289+
*
290+
* This field considers itself readonly if its parent is readonly or its own logic considers it
291+
* readonly.
292+
*/
293+
readonly readonly: Signal<boolean> = computed(
294+
() => (this.parent?.readonly() || this.logic.readonly.compute(this.fieldContext)) ?? false,
295+
);
296+
287297
/**
288298
* The submitted status of the form.
289299
*/

packages/forms/experimental/src/logic_node.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface DataDefinition {
4141
export class FieldLogicNode {
4242
readonly hidden: BooleanOrLogic;
4343
readonly disabled: BooleanOrLogic;
44+
readonly readonly: BooleanOrLogic;
4445
readonly syncErrors: ArrayMergeLogic<FormError>;
4546
readonly syncTreeErrors: ArrayMergeLogic<FormTreeError>;
4647
readonly asyncErrors: ArrayMergeLogic<FormTreeError | 'pending'>;
@@ -53,6 +54,7 @@ export class FieldLogicNode {
5354
private constructor(private predicate: Predicate | undefined) {
5455
this.hidden = new BooleanOrLogic(predicate);
5556
this.disabled = new BooleanOrLogic(predicate);
57+
this.readonly = new BooleanOrLogic(predicate);
5658
this.syncErrors = new ArrayMergeLogic<FormError>(predicate);
5759
this.syncTreeErrors = new ArrayMergeLogic<FormTreeError>(predicate);
5860
this.asyncErrors = new ArrayMergeLogic<FormTreeError | 'pending'>(predicate);
@@ -83,6 +85,7 @@ export class FieldLogicNode {
8385
// Merge standard logic.
8486
this.hidden.mergeIn(other.hidden);
8587
this.disabled.mergeIn(other.disabled);
88+
this.readonly.mergeIn(other.readonly);
8689
this.syncErrors.mergeIn(other.syncErrors);
8790

8891
// Merge data

packages/forms/experimental/test/node.spec.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {computed, Injector, signal} from '@angular/core';
2-
import {disabled, error, required, validate, validateTree} from '../src/api/logic';
2+
import {TestBed} from '@angular/core/testing';
3+
import {disabled, error, readonly, required, validate, validateTree} from '../src/api/logic';
4+
import {DISABLED_REASON, REQUIRED} from '../src/api/metadata';
35
import {apply, applyEach, form, submit} from '../src/api/structure';
46
import {FormTreeError, Schema} from '../src/api/types';
5-
import {DISABLED_REASON, REQUIRED} from '../src/api/metadata';
6-
import {TestBed} from '@angular/core/testing';
77

88
const noopSchema: Schema<unknown> = () => {};
99

@@ -244,6 +244,51 @@ describe('Node', () => {
244244
});
245245
});
246246

247+
describe('readonly', () => {
248+
it('should allow logic to make a field readonly', () => {
249+
const f = form(
250+
signal({a: 1, b: 2}),
251+
(p) => {
252+
readonly(p.a);
253+
},
254+
{injector: TestBed.inject(Injector)},
255+
);
256+
257+
expect(f.$state.readonly()).toBe(false);
258+
expect(f.a.$state.readonly()).toBe(true);
259+
expect(f.b.$state.readonly()).toBe(false);
260+
});
261+
262+
it('should allow logic to make a field conditionally readonly', () => {
263+
const f = form(
264+
signal({a: 1, b: 2}),
265+
(p) => {
266+
readonly(p.a, ({value}) => value() > 10);
267+
},
268+
{injector: TestBed.inject(Injector)},
269+
);
270+
271+
expect(f.a.$state.readonly()).toBe(false);
272+
273+
f.a.$state.value.set(11);
274+
expect(f.a.$state.readonly()).toBe(true);
275+
});
276+
277+
it('should make children of readonly parent readonly', () => {
278+
const f = form(
279+
signal({a: 1, b: 2}),
280+
(p) => {
281+
readonly(p);
282+
},
283+
{injector: TestBed.inject(Injector)},
284+
);
285+
286+
expect(f.$state.readonly()).toBe(true);
287+
expect(f.a.$state.readonly()).toBe(true);
288+
expect(f.b.$state.readonly()).toBe(true);
289+
});
290+
});
291+
247292
describe('validation', () => {
248293
it('should validate field', () => {
249294
const f = form(

0 commit comments

Comments
 (0)
0