diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 7edc9c930d5..7f76f3d2f70 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -274,7 +274,7 @@ export type MapToErrorsFn; // @public -export function max(path: SchemaPath, maxValue: number | LogicFn, config?: BaseValidatorConfig): void; +export function max(path: SchemaPath, maxValue: number | LogicFn, config?: BaseValidatorConfig): void; // @public export const MAX_LENGTH: AggregateMetadataKey; @@ -335,7 +335,7 @@ export class MetadataKey { export const MIN: AggregateMetadataKey; // @public -export function min(path: SchemaPath, minValue: number | LogicFn, config?: BaseValidatorConfig): void; +export function min(path: SchemaPath, minValue: number | LogicFn, config?: BaseValidatorConfig): void; // @public export const MIN_LENGTH: AggregateMetadataKey; diff --git a/packages/forms/signals/src/api/validators/max.ts b/packages/forms/signals/src/api/validators/max.ts index e157b996db2..f60ddfaaba4 100644 --- a/packages/forms/signals/src/api/validators/max.ts +++ b/packages/forms/signals/src/api/validators/max.ts @@ -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( - path: SchemaPath, - maxValue: number | LogicFn, - config?: BaseValidatorConfig, + path: SchemaPath, + maxValue: number | LogicFn, + config?: BaseValidatorConfig, ) { const MAX_MEMO = metadata(path, (ctx) => computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))), @@ -46,7 +46,9 @@ export function max( 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 { diff --git a/packages/forms/signals/src/api/validators/min.ts b/packages/forms/signals/src/api/validators/min.ts index 74104c9b8fb..a9bd2cdfd94 100644 --- a/packages/forms/signals/src/api/validators/min.ts +++ b/packages/forms/signals/src/api/validators/min.ts @@ -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( - path: SchemaPath, - minValue: number | LogicFn, - config?: BaseValidatorConfig, + path: SchemaPath, + minValue: number | LogicFn, + config?: BaseValidatorConfig, ) { const MIN_MEMO = metadata(path, (ctx) => computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))), @@ -46,7 +46,9 @@ export function min( 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 { diff --git a/packages/forms/signals/test/node/api/validators/max.spec.ts b/packages/forms/signals/test/node/api/validators/max.spec.ts index 5f10be453df..2188b6b375a 100644 --- a/packages/forms/signals/test/node/api/validators/max.spec.ts +++ b/packages/forms/signals/test/node/api/validators/max.spec.ts @@ -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('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('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'})]); + }); }); diff --git a/packages/forms/signals/test/node/api/validators/min.spec.ts b/packages/forms/signals/test/node/api/validators/min.spec.ts index e573fa336b1..b7132a6fe80 100644 --- a/packages/forms/signals/test/node/api/validators/min.spec.ts +++ b/packages/forms/signals/test/node/api/validators/min.spec.ts @@ -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('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('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'})]); + }); });