mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(forms): allow passing number|string|null paths to min & max (#65212)
Relaxes the constraints on which paths can be used with the `min` & `max` validation rules, since people may want to validate a potentially-null number, or a numeric value represented as a string PR Close #65212
This commit is contained in:
parent
05cf14ba44
commit
ff4633dab8
5 changed files with 74 additions and 18 deletions
|
|
@ -274,7 +274,7 @@ export type MapToErrorsFn<TValue, TResult, TPathKind extends PathKind = PathKind
|
|||
export const MAX: AggregateMetadataKey<number | undefined, number | undefined>;
|
||||
|
||||
// @public
|
||||
export function max<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<number, SchemaPathRules.Supported, TPathKind>, maxValue: number | LogicFn<number, number | undefined, TPathKind>, config?: BaseValidatorConfig<number, TPathKind>): void;
|
||||
export function max<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<number | string | null, SchemaPathRules.Supported, TPathKind>, maxValue: number | LogicFn<number | string | null, number | undefined, TPathKind>, config?: BaseValidatorConfig<number | string | null, TPathKind>): void;
|
||||
|
||||
// @public
|
||||
export const MAX_LENGTH: AggregateMetadataKey<number | undefined, number | undefined>;
|
||||
|
|
@ -335,7 +335,7 @@ export class MetadataKey<TValue> {
|
|||
export const MIN: AggregateMetadataKey<number | undefined, number | undefined>;
|
||||
|
||||
// @public
|
||||
export function min<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<number, SchemaPathRules.Supported, TPathKind>, minValue: number | LogicFn<number, number | undefined, TPathKind>, config?: BaseValidatorConfig<number, TPathKind>): void;
|
||||
export function min<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<number | string | null, SchemaPathRules.Supported, TPathKind>, minValue: number | LogicFn<number | string | null, number | undefined, TPathKind>, config?: BaseValidatorConfig<number | string | null, TPathKind>): void;
|
||||
|
||||
// @public
|
||||
export const MIN_LENGTH: AggregateMetadataKey<number | undefined, number | undefined>;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {computed} from '@angular/core';
|
||||
import {aggregateMetadata, metadata, validate} from '../logic';
|
||||
import {MAX} from '../metadata';
|
||||
import {SchemaPath, SchemaPathRules, LogicFn, PathKind} from '../types';
|
||||
import {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
|
||||
import {maxError} from '../validation_errors';
|
||||
import {BaseValidatorConfig, getOption, isEmpty} from './util';
|
||||
|
||||
|
|
@ -30,9 +30,9 @@ import {BaseValidatorConfig, getOption, isEmpty} from './util';
|
|||
* @experimental 21.0.0
|
||||
*/
|
||||
export function max<TPathKind extends PathKind = PathKind.Root>(
|
||||
path: SchemaPath<number, SchemaPathRules.Supported, TPathKind>,
|
||||
maxValue: number | LogicFn<number, number | undefined, TPathKind>,
|
||||
config?: BaseValidatorConfig<number, TPathKind>,
|
||||
path: SchemaPath<number | string | null, SchemaPathRules.Supported, TPathKind>,
|
||||
maxValue: number | LogicFn<number | string | null, number | undefined, TPathKind>,
|
||||
config?: BaseValidatorConfig<number | string | null, TPathKind>,
|
||||
) {
|
||||
const MAX_MEMO = metadata(path, (ctx) =>
|
||||
computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))),
|
||||
|
|
@ -46,7 +46,9 @@ export function max<TPathKind extends PathKind = PathKind.Root>(
|
|||
if (max === undefined || Number.isNaN(max)) {
|
||||
return undefined;
|
||||
}
|
||||
if (ctx.value() > max) {
|
||||
const value = ctx.value();
|
||||
const numValue = !value && value !== 0 ? NaN : Number(value); // Treat `''` and `null` as `NaN`
|
||||
if (numValue > max) {
|
||||
if (config?.error) {
|
||||
return getOption(config.error, ctx);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {computed} from '@angular/core';
|
||||
import {aggregateMetadata, metadata, validate} from '../logic';
|
||||
import {MIN} from '../metadata';
|
||||
import {SchemaPath, LogicFn, PathKind, SchemaPathRules} from '../types';
|
||||
import {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
|
||||
import {minError} from '../validation_errors';
|
||||
import {BaseValidatorConfig, getOption, isEmpty} from './util';
|
||||
|
||||
|
|
@ -30,9 +30,9 @@ import {BaseValidatorConfig, getOption, isEmpty} from './util';
|
|||
* @experimental 21.0.0
|
||||
*/
|
||||
export function min<TPathKind extends PathKind = PathKind.Root>(
|
||||
path: SchemaPath<number, SchemaPathRules.Supported, TPathKind>,
|
||||
minValue: number | LogicFn<number, number | undefined, TPathKind>,
|
||||
config?: BaseValidatorConfig<number, TPathKind>,
|
||||
path: SchemaPath<number | string | null, SchemaPathRules.Supported, TPathKind>,
|
||||
minValue: number | LogicFn<number | string | null, number | undefined, TPathKind>,
|
||||
config?: BaseValidatorConfig<number | string | null, TPathKind>,
|
||||
) {
|
||||
const MIN_MEMO = metadata(path, (ctx) =>
|
||||
computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))),
|
||||
|
|
@ -46,7 +46,9 @@ export function min<TPathKind extends PathKind = PathKind.Root>(
|
|||
if (min === undefined || Number.isNaN(min)) {
|
||||
return undefined;
|
||||
}
|
||||
if (ctx.value() < min) {
|
||||
const value = ctx.value();
|
||||
const numValue = !value && value !== 0 ? NaN : Number(value); // Treat `''` and `null` as `NaN`
|
||||
if (numValue < min) {
|
||||
if (config?.error) {
|
||||
return getOption(config.error, ctx);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ describe('max validator', () => {
|
|||
(p) => {
|
||||
max(p.age, 5, {
|
||||
error: ({value}) => {
|
||||
return customError({kind: 'special-max', message: value().toString()});
|
||||
return customError({kind: 'special-max', message: value()?.toString()});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -104,7 +104,7 @@ describe('max validator', () => {
|
|||
error: ({value, valueOf}) => {
|
||||
return valueOf(p.name) === 'disabled'
|
||||
? []
|
||||
: customError({kind: 'special-max', message: value().toString()});
|
||||
: customError({kind: 'special-max', message: value()?.toString()});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -155,7 +155,7 @@ describe('max validator', () => {
|
|||
(p) => {
|
||||
max(p.age, 5, {
|
||||
error: ({value}) => {
|
||||
return customError({kind: 'special-max', message: value().toString()});
|
||||
return customError({kind: 'special-max', message: value()?.toString()});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -299,4 +299,30 @@ describe('max validator', () => {
|
|||
expect(f.age().errors()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate properly formatted strings', () => {
|
||||
const f = form(
|
||||
signal<number | string | null>('4'),
|
||||
(p) => {
|
||||
max(p, -10);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
expect(f().errors()).toEqual([jasmine.objectContaining({kind: 'max'})]);
|
||||
});
|
||||
|
||||
it('should not validate improperly formatted strings or null', () => {
|
||||
const f = form(
|
||||
signal<number | string | null>('4f'),
|
||||
(p) => {
|
||||
max(p, -10);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
expect(f().errors()).toEqual([]);
|
||||
f().value.set(null);
|
||||
expect(f().errors()).toEqual([]);
|
||||
f().value.set(4);
|
||||
expect(f().errors()).toEqual([jasmine.objectContaining({kind: 'max'})]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ describe('min validator', () => {
|
|||
(p) => {
|
||||
min(p.age, 5, {
|
||||
error: ({value}) => {
|
||||
return customError({kind: 'special-min', message: value().toString()});
|
||||
return customError({kind: 'special-min', message: value()?.toString()});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -84,7 +84,7 @@ describe('min validator', () => {
|
|||
error: ({value}) => {
|
||||
return {
|
||||
kind: 'special-min',
|
||||
message: value().toString(),
|
||||
message: value()?.toString(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -130,7 +130,7 @@ describe('min validator', () => {
|
|||
error: ({value, valueOf}) => {
|
||||
return valueOf(p.name) === 'disabled'
|
||||
? []
|
||||
: customError({kind: 'special-min', message: value().toString()});
|
||||
: customError({kind: 'special-min', message: value()?.toString()});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
@ -308,4 +308,30 @@ describe('min validator', () => {
|
|||
expect(f.age().errors()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate properly formatted strings', () => {
|
||||
const f = form(
|
||||
signal<number | string | null>('4'),
|
||||
(p) => {
|
||||
min(p, 10);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
expect(f().errors()).toEqual([jasmine.objectContaining({kind: 'min'})]);
|
||||
});
|
||||
|
||||
it('should not validate improperly formatted strings or null', () => {
|
||||
const f = form(
|
||||
signal<number | string | null>('4f'),
|
||||
(p) => {
|
||||
min(p, 10);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
expect(f().errors()).toEqual([]);
|
||||
f().value.set(null);
|
||||
expect(f().errors()).toEqual([]);
|
||||
f().value.set(4);
|
||||
expect(f().errors()).toEqual([jasmine.objectContaining({kind: 'min'})]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue