refactor(forms): use when consistently for conditional rules and validators

This commit updates the signal forms API to use a consistent 'when' parameter for conditional rules and validators, replacing direct function arguments.
This commit is contained in:
kirjs 2026-05-04 14:19:11 -04:00 committed by Alex Rickabaugh
parent 7745365910
commit df54e6a7b2
23 changed files with 197 additions and 130 deletions

View file

@ -15,8 +15,9 @@ import type {FieldContext, LogicFn, PathKind, SchemaPath, SchemaPathRules} from
* validation, touched/dirty, or other state of its parent field.
*
* @param path The target path to add the disabled logic to.
* @param logic A reactive function that returns `true` (or a string reason) when the field is disabled,
* and `false` when it is not disabled.
* @param config Optional configuration object.
* - `when`: A reactive function that returns `true` (or a string reason) when the field is disabled,
* and `false` when it is not disabled. Can also be a static string reason.
* @template TValue The type of value stored in the field the logic is bound to.
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
*
@ -25,17 +26,17 @@ import type {FieldContext, LogicFn, PathKind, SchemaPath, SchemaPathRules} from
*/
export function disabled<TValue, TPathKind extends PathKind = PathKind.Root>(
path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,
logic?: string | NoInfer<LogicFn<TValue, boolean | string, TPathKind>>,
config?: {when?: string | NoInfer<LogicFn<TValue, boolean | string, TPathKind>>},
): void {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
pathNode.builder.addDisabledReasonRule((ctx) => {
let result: boolean | string = true;
if (typeof logic === 'string') {
result = logic;
} else if (logic) {
result = logic(ctx as FieldContext<TValue, TPathKind>);
if (typeof config?.when === 'string') {
result = config.when;
} else if (config?.when) {
result = config.when(ctx as FieldContext<TValue, TPathKind>);
}
if (typeof result === 'string') {
return {fieldTree: ctx.fieldTree, message: result};

View file

@ -23,7 +23,8 @@ import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
* ```
*
* @param path The target path to add the hidden logic to.
* @param logic A reactive function that returns `true` when the field is hidden.
* @param config Options object containing the `when` condition.
* - `when`: A reactive function that returns `true` when the field is hidden.
* @template TValue The type of value stored in the field the logic is bound to.
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
*
@ -32,10 +33,10 @@ import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
*/
export function hidden<TValue, TPathKind extends PathKind = PathKind.Root>(
path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,
logic: NoInfer<LogicFn<TValue, boolean, TPathKind>>,
config: {when: NoInfer<LogicFn<TValue, boolean, TPathKind>>},
): void {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
pathNode.builder.addHiddenRule(logic);
pathNode.builder.addHiddenRule(config.when);
}

View file

@ -15,7 +15,8 @@ import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
* the validation, touched/dirty, or other state of its parent field.
*
* @param path The target path to make readonly.
* @param logic A reactive function that returns `true` when the field is readonly.
* @param config Optional configuration object.
* - `when`: A reactive function that returns `true` when the field is readonly.
* @template TValue The type of value stored in the field the logic is bound to.
* @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
*
@ -24,10 +25,10 @@ import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
*/
export function readonly<TValue, TPathKind extends PathKind = PathKind.Root>(
path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,
logic: NoInfer<LogicFn<TValue, boolean, TPathKind>> = () => true,
config?: {when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>},
) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
pathNode.builder.addReadonlyRule(logic);
pathNode.builder.addReadonlyRule(config?.when ?? (() => true));
}

View file

@ -63,6 +63,9 @@ export function email<TPathKind extends PathKind = PathKind.Root>(
config?: BaseValidatorConfig<string, TPathKind>,
) {
validate(path, (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
if (isEmpty(ctx.value())) {
return undefined;
}

View file

@ -38,15 +38,18 @@ export function max<TValue extends number | null, TPathKind extends PathKind = P
const MAX_MEMO = createMetadataKey<number | undefined>();
// Memoize the maximum valid value.
metadata(path, MAX_MEMO, (ctx) => (typeof maxValue === 'function' ? maxValue(ctx) : maxValue));
metadata(path, MAX_MEMO, (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof maxValue === 'function' ? maxValue(ctx) : maxValue;
});
// Publish the memoized maximum value for aggregation.
metadata(path, MAX_NUMBER, ({state}) => state.metadata(MAX_MEMO)!());
// Use `MAX_NUMBER` to define the `max` property of the field.
metadata(path, MAX, () => MAX_NUMBER as LimitKey<TValue>);
// Validate that the field value is not greater than the maximum value.
validate(path, (ctx) => {
const value = ctx.value();
if (value === null || Number.isNaN(value)) {

View file

@ -44,9 +44,12 @@ export function maxLength<
maxLength: number | LogicFn<TValue, number | undefined, TPathKind>,
config?: BaseValidatorConfig<TValue, TPathKind>,
) {
const MAX_LENGTH_MEMO = metadata(path, createMetadataKey<number | undefined>(), (ctx) =>
typeof maxLength === 'number' ? maxLength : maxLength(ctx),
);
const MAX_LENGTH_MEMO = metadata(path, createMetadataKey<number | undefined>(), (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof maxLength === 'number' ? maxLength : maxLength(ctx);
});
metadata(path, MAX_LENGTH, ({state}) => state.metadata(MAX_LENGTH_MEMO)!());
validate(path, (ctx) => {
if (isEmpty(ctx.value())) {

View file

@ -38,15 +38,18 @@ export function min<TValue extends number | null, TPathKind extends PathKind = P
const MIN_MEMO = createMetadataKey<number | undefined>();
// Memomize the minimum valid.
metadata(path, MIN_MEMO, (ctx) => (typeof minValue === 'function' ? minValue(ctx) : minValue));
metadata(path, MIN_MEMO, (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof minValue === 'function' ? minValue(ctx) : minValue;
});
// Publish the memoized mininum value for aggregation.
metadata(path, MIN_NUMBER, ({state}) => state.metadata(MIN_MEMO)!());
// Use `MIN_NUMBER` to define the `min` property of the field.
metadata(path, MIN, () => MIN_NUMBER as LimitKey<TValue>);
// Validate that the field value is not less than the minimum value.
validate(path, (ctx) => {
const value = ctx.value();
if (value === null || Number.isNaN(value)) {

View file

@ -44,9 +44,12 @@ export function minLength<
minLength: number | LogicFn<TValue, number | undefined, TPathKind>,
config?: BaseValidatorConfig<TValue, TPathKind>,
) {
const MIN_LENGTH_MEMO = metadata(path, createMetadataKey<number | undefined>(), (ctx) =>
typeof minLength === 'number' ? minLength : minLength(ctx),
);
const MIN_LENGTH_MEMO = metadata(path, createMetadataKey<number | undefined>(), (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof minLength === 'number' ? minLength : minLength(ctx);
});
metadata(path, MIN_LENGTH, ({state}) => state.metadata(MIN_LENGTH_MEMO)!());
validate(path, (ctx) => {
if (isEmpty(ctx.value())) {

View file

@ -33,9 +33,12 @@ export function pattern<TPathKind extends PathKind = PathKind.Root>(
pattern: RegExp | LogicFn<string | undefined, RegExp | undefined, TPathKind>,
config?: BaseValidatorConfig<string, TPathKind>,
) {
const PATTERN_MEMO = metadata(path, createMetadataKey<RegExp | undefined>(), (ctx) =>
pattern instanceof RegExp ? pattern : pattern(ctx),
);
const PATTERN_MEMO = metadata(path, createMetadataKey<RegExp | undefined>(), (ctx) => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return pattern instanceof RegExp ? pattern : pattern(ctx);
});
metadata(path, PATTERN, ({state}) => state.metadata(PATTERN_MEMO)!());
validate(path, (ctx) => {
if (isEmpty(ctx.value())) {

View file

@ -18,6 +18,8 @@ export type BaseValidatorConfig<TValue, TPathKind extends PathKind = PathKind.Ro
/** A user-facing error message to include with the error. */
message?: string | LogicFn<TValue, string, TPathKind>;
error?: never;
/** A function that receives the `FieldContext` and returns true if the validator should be applied. */
when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>;
}
| {
/**
@ -26,6 +28,8 @@ export type BaseValidatorConfig<TValue, TPathKind extends PathKind = PathKind.Ro
*/
error?: OneOrMany<ValidationError> | LogicFn<TValue, OneOrMany<ValidationError>, TPathKind>;
message?: never;
/** A function that receives the `FieldContext` and returns true if the validator should be applied. */
when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>;
};
/** Gets the length or size of the given value. */

View file

@ -21,6 +21,7 @@ import {FieldPathNode} from '../../../schema/path_node';
import {assertPathIsCurrent} from '../../../schema/schema';
import {
FieldContext,
LogicFn,
PathKind,
SchemaPath,
SchemaPathRules,
@ -107,6 +108,10 @@ export interface AsyncValidatorOptions<
* If a field is not given, the error is assumed to apply to the field being validated.
*/
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
/**
* A function that receives the field context and returns true if the async validation should be run.
*/
readonly when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>;
}
/**
@ -149,6 +154,9 @@ export function validateAsync<TValue, TParams, TResult, TPathKind extends PathKi
if (validationState.shouldSkipValidation() || !validationState.syncValid()) {
return undefined;
}
if (opts.when && !opts.when(ctx)) {
return undefined;
}
return opts.params(ctx);
});

View file

@ -10,10 +10,11 @@ import {httpResource, HttpResourceOptions, HttpResourceRequest} from '@angular/c
import {DebounceTimer, ResourceSnapshot, Signal} from '@angular/core';
import {
FieldContext,
SchemaPath,
LogicFn,
PathKind,
TreeValidationResult,
SchemaPath,
SchemaPathRules,
TreeValidationResult,
} from '../../types';
import {MapToErrorsFn, validateAsync} from './validate_async';
@ -68,6 +69,10 @@ export interface HttpValidatorOptions<TValue, TResult, TPathKind extends PathKin
* returns a promise that resolves when the update should proceed.
*/
readonly debounce?: DebounceTimer<string | HttpResourceRequest | undefined>;
/**
* A function that receives the field context and returns true if the async validation should be run.
*/
readonly when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>;
}
/**
@ -96,5 +101,6 @@ export function validateHttp<TValue, TResult = unknown, TPathKind extends PathKi
factory: (request: Signal<any>) => httpResource(request, opts.options),
onSuccess: opts.onSuccess,
onError: opts.onError,
when: opts.when,
});
}

View file

@ -16,8 +16,10 @@ describe('hidden', () => {
const f = form(
cat,
(p) => {
hidden(p, ({value}) => {
return value.name === 'hidden-cat';
hidden(p, {
when: ({value}) => {
return value.name === 'hidden-cat';
},
});
},
{injector: TestBed.inject(Injector)},
@ -32,8 +34,10 @@ describe('hidden', () => {
const f = form(
cat,
(p) => {
hidden(p.name, ({value}) => {
return value() === 'hidden-cat';
hidden(p.name, {
when: ({value}) => {
return value() === 'hidden-cat';
},
});
},
{injector: TestBed.inject(Injector)},
@ -48,8 +52,10 @@ describe('hidden', () => {
const f = form(
cat,
(p) => {
hidden(p, ({value}) => {
return value().name === 'hidden-cat';
hidden(p, {
when: ({value}) => {
return value().name === 'hidden-cat';
},
});
},
{injector: TestBed.inject(Injector)},
@ -65,8 +71,10 @@ describe('hidden', () => {
const f = form(
cat,
(p) => {
hidden(p.name, ({value}) => {
return value() === 'hidden-cat';
hidden(p.name, {
when: ({value}) => {
return value() === 'hidden-cat';
},
});
validate(p.name, () => {
@ -94,8 +102,10 @@ describe('hidden', () => {
const f = form(
cat,
(p) => {
hidden(p.name, ({value}) => {
return value() === 'hidden-cat';
hidden(p.name, {
when: ({value}) => {
return value() === 'hidden-cat';
},
});
},
{injector: TestBed.inject(Injector)},

View file

@ -254,8 +254,10 @@ describe('Forms compat', () => {
const f = compatForm(
cat,
(p) => {
disabled(p, ({value}) => {
return value().name === 'disabled-cat';
disabled(p, {
when: ({value}) => {
return value().name === 'disabled-cat';
},
});
},
{
@ -286,8 +288,10 @@ describe('Forms compat', () => {
const f = compatForm(
cat,
(p) => {
hidden(p, ({value}) => {
return value().name === 'hidden-cat';
hidden(p, {
when: ({value}) => {
return value().name === 'hidden-cat';
},
});
},
{
@ -717,8 +721,10 @@ describe('Forms compat', () => {
return valueOf(path.age) < 8 ? [] : [];
});
readonly(path.name, ({valueOf}) => {
return valueOf(path.age) < 8;
readonly(path.name, {
when: ({valueOf}) => {
return valueOf(path.age) < 8;
},
});
email(path.name, {

View file

@ -227,7 +227,7 @@ describe('extractValue', () => {
signal(model),
(p) => {
applyEach(p.items, (item) => {
disabled(item, ({value}) => value() === 2 || value() === 4);
disabled(item, {when: ({value}) => value() === 2 || value() === 4});
});
},
{injector},
@ -242,7 +242,7 @@ describe('extractValue', () => {
signal(model),
(p) => {
applyEach(p.items, (item) => {
disabled(item, ({value}) => value() !== 30);
disabled(item, {when: ({value}) => value() !== 30});
});
},
{injector},

View file

@ -140,7 +140,7 @@ describe('SignalFormControl', () => {
it('should support disabled via rules', () => {
const form = createSignalFormControl(10, (p) => {
disabled(p, ({value}) => value() > 15);
disabled(p, {when: ({value}) => value() > 15});
});
expect(form.disabled).toBe(false);
@ -656,7 +656,7 @@ describe('SignalFormControl', () => {
describe('callback registration', () => {
it('should call registered onDisabledChange callback when disabled state changes', () => {
const form = createSignalFormControl(10, (p) => {
disabled(p, ({value}) => value() > 15);
disabled(p, {when: ({value}) => value() > 15});
});
const callback = jasmine.createSpy('onDisabledChange');
@ -674,7 +674,7 @@ describe('SignalFormControl', () => {
it('should NOT track signals read inside onDisabledChange callback', () => {
const form = createSignalFormControl(10, (p) => {
disabled(p, ({value}) => value() > 15);
disabled(p, {when: ({value}) => value() > 15});
});
const otherSignal = signal(0);
const callback = jasmine.createSpy('onDisabledChange').and.callFake(() => {

View file

@ -288,7 +288,7 @@ describe('FieldNode', () => {
b: 2,
}),
(p) => {
hidden(p, () => true);
hidden(p, {when: () => true});
},
{injector: TestBed.inject(Injector)},
);
@ -353,7 +353,7 @@ describe('FieldNode', () => {
b: 2,
}),
(p) => {
readonly(p, isReadonly);
readonly(p, {when: isReadonly});
},
{injector: TestBed.inject(Injector)},
);
@ -537,7 +537,7 @@ describe('FieldNode', () => {
b: 2,
}),
(p) => {
hidden(p, () => true);
hidden(p, {when: () => true});
},
{injector: TestBed.inject(Injector)},
);
@ -602,7 +602,7 @@ describe('FieldNode', () => {
b: 2,
}),
(p) => {
hidden(p, isHidden);
hidden(p, {when: isHidden});
},
{injector: TestBed.inject(Injector)},
);
@ -669,11 +669,13 @@ describe('FieldNode', () => {
signal({names: [{name: 'Alex'}, {name: 'Miles'}]}),
(p) => {
applyEach(p.names, (a) => {
disabled(a.name, ({value, fieldTreeOf}) => {
const el = fieldTreeOf(a);
expect(el().value().name).toBe(value());
expect([...fieldTreeOf(p).names].findIndex((e: any) => e === el)).not.toBe(-1);
return true;
disabled(a.name, {
when: ({value, fieldTreeOf}) => {
const el = fieldTreeOf(a);
expect(el().value().name).toBe(value());
expect([...fieldTreeOf(p).names].findIndex((e: any) => e === el)).not.toBe(-1);
return true;
},
});
});
},
@ -689,7 +691,7 @@ describe('FieldNode', () => {
(p) => {
applyEach(p, (a) => {
a;
disabled(a, ({value}) => value() % 2 === 0);
disabled(a, {when: ({value}) => value() % 2 === 0});
});
},
{injector: TestBed.inject(Injector)},
@ -706,7 +708,7 @@ describe('FieldNode', () => {
(p) => {
applyEach(p, (el) => {
// Disabled if even.
disabled(el, ({value}) => value() % 2 === 0);
disabled(el, {when: ({value}) => value() % 2 === 0});
});
},
{injector: TestBed.inject(Injector)},
@ -802,7 +804,7 @@ describe('FieldNode', () => {
const f = form(
signal({a: 1, b: 2}),
(p) => {
disabled(p.a, ({value}) => value() !== 2);
disabled(p.a, {when: ({value}) => value() !== 2});
},
{injector: TestBed.inject(Injector)},
);
@ -820,7 +822,7 @@ describe('FieldNode', () => {
const f = form(
signal({a: 1, b: 2}),
(p) => {
disabled(p.a, () => 'a cannot be changed');
disabled(p.a, {when: () => 'a cannot be changed'});
},
{injector: TestBed.inject(Injector)},
);
@ -838,7 +840,7 @@ describe('FieldNode', () => {
const f = form(
signal({a: 1, b: 2}),
(p) => {
disabled(p.a, ({value}) => (value() > 5 ? 'a cannot be changed' : false));
disabled(p.a, {when: ({value}) => (value() > 5 ? 'a cannot be changed' : false)});
},
{injector: TestBed.inject(Injector)},
);
@ -861,7 +863,7 @@ describe('FieldNode', () => {
const f = form(
signal({a: 1, b: 2}),
(p) => {
disabled(p, () => 'form unavailable');
disabled(p, {when: () => 'form unavailable'});
},
{injector: TestBed.inject(Injector)},
);
@ -887,7 +889,7 @@ describe('FieldNode', () => {
signal({a: '', b: ''}),
(p) => {
disabled(p.a);
disabled(p.b, 'disabled!');
disabled(p.b, {when: 'disabled!'});
},
{injector: TestBed.inject(Injector)},
);
@ -925,7 +927,7 @@ describe('FieldNode', () => {
const f = form(
signal({a: 1, b: 2}),
(p) => {
readonly(p.a, ({value}) => value() > 10);
readonly(p.a, {when: ({value}) => value() > 10});
},
{injector: TestBed.inject(Injector)},
);
@ -955,7 +957,7 @@ describe('FieldNode', () => {
const f = form(
signal(''),
(p) => {
readonly(p, isReadonly);
readonly(p, {when: isReadonly});
required(p);
},
{injector: TestBed.inject(Injector)},
@ -1390,7 +1392,7 @@ describe('FieldNode', () => {
}
const addressSchema: SchemaOrSchemaFn<Address> = (p) => {
disabled(p.street, () => true);
disabled(p.street, {when: () => true});
};
const data = signal<{name: string; address: Address}>({
@ -1430,7 +1432,7 @@ describe('FieldNode', () => {
it('should resolve predefined schema paths within the local context', () => {
const s = schema<{a: string; b: string}>((p) => {
disabled(p.b, ({valueOf}) => valueOf(p.a) === 'disable-b');
disabled(p.b, {when: ({valueOf}) => valueOf(p.a) === 'disable-b'});
});
const f = form(
@ -1448,7 +1450,7 @@ describe('FieldNode', () => {
it('should resolve predefined schema paths deeply nested within the schema', () => {
const s = schema<{a: string; b: string}>((p) => {
disabled(p.b, ({valueOf}) => valueOf(p.a) === 'disable-b');
disabled(p.b, {when: ({valueOf}) => valueOf(p.a) === 'disable-b'});
});
const f = form(
@ -1472,9 +1474,11 @@ describe('FieldNode', () => {
const f = form(
signal(''),
(p) => {
disabled(p, ({fieldTreeOf}) => {
fieldTreeOf(otherP);
return true;
disabled(p, {
when: ({fieldTreeOf}) => {
fieldTreeOf(otherP);
return true;
},
});
},
{injector: TestBed.inject(Injector)},

View file

@ -34,8 +34,10 @@ function isNonNull<T>(t: T): t is NonNullable<T> {
describe('recursive schema logic', () => {
it('should support recursive logic', () => {
const s = schema<TreeData>((p) => {
disabled(p.level, ({valueOf}) => {
return valueOf(p.level) % 2 === 0;
disabled(p.level, {
when: ({valueOf}) => {
return valueOf(p.level) % 2 === 0;
},
});
applyWhenValue(p.next, isNonNull, s);
});
@ -58,11 +60,11 @@ describe('recursive schema logic', () => {
it('should support co-recursive logic', () => {
const s1: Schema<TreeData> = schema((p) => {
disabled(p.level, ({valueOf}) => valueOf(p.level) % 2 === 0);
disabled(p.level, {when: ({valueOf}) => valueOf(p.level) % 2 === 0});
applyWhenValue(p.next, isNonNull, s2);
});
const s2: Schema<TreeData> = schema((p) => {
disabled(p.level, ({valueOf}) => valueOf(p.level) % 2 === 0);
disabled(p.level, {when: ({valueOf}) => valueOf(p.level) % 2 === 0});
applyWhenValue(p.next, isNonNull, s1);
});
const f = form<TreeData>(

View file

@ -423,7 +423,7 @@ describe('submit', () => {
data,
(name) => {
// Disable first name when last name is empty.
disabled(name.first, ({valueOf}) => valueOf(name.last) === '');
disabled(name.first, {when: ({valueOf}) => valueOf(name.last) === ''});
},
{injector: TestBed.inject(Injector)},
);
@ -448,7 +448,7 @@ describe('submit', () => {
data,
(name) => {
// Hide first name when last name is empty.
hidden(name.first, ({valueOf}) => valueOf(name.last) === '');
hidden(name.first, {when: ({valueOf}) => valueOf(name.last) === ''});
},
{injector: TestBed.inject(Injector)},
);
@ -473,7 +473,7 @@ describe('submit', () => {
data,
(name) => {
// Make first name readonly when last name is empty.
readonly(name.first, ({valueOf}) => valueOf(name.last) === '');
readonly(name.first, {when: ({valueOf}) => valueOf(name.last) === ''});
},
{injector: TestBed.inject(Injector)},
);

View file

@ -73,7 +73,7 @@ describe('createComponent', () => {
const environmentInjector = TestBed.inject(EnvironmentInjector);
const control = TestBed.runInInjectionContext(() => {
return form(signal('initial value'), (p) => {
disabled(p, disabledSignal);
disabled(p, {when: disabledSignal});
});
});

View file

@ -324,7 +324,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
}
@ -359,7 +359,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(CustomControlDir);
}
@ -400,7 +400,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(BaseControlDir);
}
@ -431,7 +431,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -456,7 +456,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -482,7 +482,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -509,7 +509,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly dir = viewChild.required(TestDir);
}
@ -541,7 +541,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(false), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly dir = viewChild.required(TestDir);
}
@ -620,7 +620,7 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -663,7 +663,7 @@ describe('field directive', () => {
class TestCmp {
readonly data = signal('');
readonly f = form(this.data, (p) => {
disabled(p, () => 'Currently unavailable');
disabled(p, {when: () => 'Currently unavailable'});
});
readonly customControl = viewChild.required(CustomControlDir);
}
@ -694,7 +694,7 @@ describe('field directive', () => {
class TestCmp {
readonly data = signal('');
readonly f = form(this.data, (p) => {
disabled(p, () => 'Currently unavailable');
disabled(p, {when: () => 'Currently unavailable'});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -724,7 +724,7 @@ describe('field directive', () => {
class TestCmp {
readonly data = signal('');
readonly f = form(this.data, (p) => {
disabled(p, () => 'Currently unavailable');
disabled(p, {when: () => 'Currently unavailable'});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -750,8 +750,10 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, () => {
return this.disabled() ? 'b' : false;
disabled(p, {
when: () => {
return this.disabled() ? 'b' : false;
},
});
});
readonly dir = viewChild.required(TestDir);
@ -790,8 +792,10 @@ describe('field directive', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, () => {
return this.disabled() ? 'b' : false;
disabled(p, {
when: () => {
return this.disabled() ? 'b' : false;
},
});
});
readonly dir = viewChild.required(TestDir);
@ -822,7 +826,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal({x: '', y: ''}), (p) => {
disabled(p.x, () => 'Currently unavailable');
disabled(p.x, {when: () => 'Currently unavailable'});
});
readonly field = signal(this.f.x);
readonly customControl = viewChild.required(CustomControl);
@ -1053,7 +1057,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal(''), (p) => {
hidden(p, () => !visible());
hidden(p, {when: () => !visible()});
});
readonly field = signal(this.f);
readonly customControl = viewChild.required(CustomControlDir);
@ -1086,7 +1090,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal(''), (p) => {
hidden(p, () => !visible());
hidden(p, {when: () => !visible()});
});
readonly field = signal(this.f);
readonly customControl = viewChild.required(CustomControl);
@ -1115,7 +1119,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal(''), (p) => {
hidden(p, () => !visible());
hidden(p, {when: () => !visible()});
});
readonly field = signal(this.f);
readonly customControl = viewChild.required(CustomControl);
@ -1142,7 +1146,7 @@ describe('field directive', () => {
class TestCmp {
readonly hidden = signal(false);
readonly f = form(signal(''), (p) => {
hidden(p, this.hidden);
hidden(p, {when: this.hidden});
});
readonly dir = viewChild.required(TestDir);
}
@ -1174,7 +1178,7 @@ describe('field directive', () => {
class TestCmp {
readonly hidden = signal(false);
readonly f = form(signal(''), (p) => {
hidden(p, this.hidden);
hidden(p, {when: this.hidden});
});
readonly dir = viewChild.required(TestDir);
}
@ -1201,7 +1205,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal({x: 'a', y: 'b'}), (p) => {
hidden(p.x, () => true);
hidden(p.x, {when: () => true});
});
readonly field = signal(this.f.x);
readonly customControl = viewChild.required(CustomControl);
@ -1223,7 +1227,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal(''), (p) => {
hidden(p, () => true);
hidden(p, {when: () => true});
});
}
@ -1246,7 +1250,7 @@ describe('field directive', () => {
})
class TestCmp {
readonly f = form(signal(''), (p) => {
hidden(p, isHidden);
hidden(p, {when: isHidden});
});
}
@ -1866,7 +1870,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(true);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
}
@ -1901,7 +1905,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
readonly child = viewChild.required(CustomControlDir);
}
@ -1932,7 +1936,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
readonly customControl = viewChild.required(CustomControl);
}
@ -1959,7 +1963,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
readonly child = viewChild.required(CustomControl);
}
@ -1985,7 +1989,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
}
@ -2011,7 +2015,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
readonly dir = viewChild.required(TestDir);
}
@ -2043,7 +2047,7 @@ describe('field directive', () => {
class TestCmp {
readonly readonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.readonly);
readonly(p, {when: this.readonly});
});
readonly dir = viewChild.required(TestDir);
}
@ -4690,7 +4694,7 @@ describe('field directive', () => {
`,
})
class TestCmp {
f = form(signal(''), (p) => hidden(p, ({value}) => value() === ''));
f = form(signal(''), (p) => hidden(p, {when: ({value}) => value() === ''}));
select = viewChild<ElementRef<HTMLSelectElement>>('select');
options = ['one', 'two', 'three'];
}
@ -4721,7 +4725,7 @@ describe('field directive', () => {
`,
})
class TestCmp {
f = form(signal(''), (p) => hidden(p, ({value}) => value() === ''));
f = form(signal(''), (p) => hidden(p, {when: ({value}) => value() === ''}));
select = viewChild<ElementRef<HTMLSelectElement>>('select');
options = ['one', 'two', 'three'];
}
@ -4934,7 +4938,7 @@ describe('field directive', () => {
myInput = viewChild.required<CustomInput>(CustomInput);
data = signal('');
f = form(this.data, (p) => {
disabled(p, () => 'Currently unavailable');
disabled(p, {when: () => 'Currently unavailable'});
});
}
@ -4991,7 +4995,7 @@ describe('field directive', () => {
myInput = viewChild.required<CustomInput>(CustomInput);
data = signal('');
f = form(this.data, (p) => {
hidden(p, ({value}) => value() === '');
hidden(p, {when: ({value}) => value() === ''});
});
}
@ -5164,7 +5168,7 @@ describe('field directive', () => {
model = signal('');
f = form(this.model, (p) => {
required(p, {message: 'schema error'});
disabled(p, ({value}) => (value() === 'disabled' ? 'schema disabled' : false));
disabled(p, {when: ({value}) => (value() === 'disabled' ? 'schema disabled' : false)});
});
disabledReasons = [{message: 'manual disabled'}];
errors = [{kind: 'error', message: 'manual error'}];

View file

@ -388,7 +388,7 @@ describe('ControlValueAccessor', () => {
})
class TestCmp {
f = form<string>(signal('test'), (p) => {
disabled(p, () => !enabled());
disabled(p, {when: () => !enabled()});
});
}
@ -516,7 +516,7 @@ describe('ControlValueAccessor', () => {
class App {
disabled = signal(false);
readonly f = form(signal('test'), (f) => {
disabled(f, () => this.disabled());
disabled(f, {when: () => this.disabled()});
});
}
@ -622,7 +622,7 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
readonly dir = viewChild.required(TestDir);
}
@ -644,7 +644,7 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, this.disabled);
disabled(p, {when: this.disabled});
});
}
@ -725,8 +725,10 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly disabled = signal(false);
readonly f = form(signal(''), (p) => {
disabled(p, () => {
return this.disabled() ? 'Test reason' : false;
disabled(p, {
when: () => {
return this.disabled() ? 'Test reason' : false;
},
});
});
readonly dir = viewChild.required(TestDir);
@ -787,7 +789,7 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly hidden = signal(false);
readonly f = form(signal(''), (p) => {
hidden(p, this.hidden);
hidden(p, {when: this.hidden});
});
readonly dir = viewChild.required(TestDir);
}
@ -923,7 +925,7 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly isReadonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.isReadonly);
readonly(p, {when: this.isReadonly});
});
readonly dir = viewChild.required(TestDir);
}
@ -945,7 +947,7 @@ describe('ControlValueAccessor', () => {
class TestCmp {
readonly isReadonly = signal(false);
readonly f = form(signal(''), (p) => {
readonly(p, this.isReadonly);
readonly(p, {when: this.isReadonly});
});
}

View file

@ -103,7 +103,7 @@ describe('SignalFormControl (web)', () => {
readonly signalControl = new SignalFormControl(
10,
(p) => {
disabled(p, ({value}) => value() > 15);
disabled(p, {when: ({value}) => value() > 15});
},
{injector: inject(Injector)},
);