mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
The `action` and `onInvalid` handlers now recevie two pieces of information: 1. The form that is being submitted 2. The specific field that the submit was triggered on Remove the `submit()` method on field state - supporting this is complex from a typing perspective, since the `FieldState` only knows its `TValue` type, not the `TModel` type of its owning `FieldTree`. Rather than try to pack additional generics on to `FieldState`, we'll just leave the `submit` function as a standalone importable function.
826 lines
23 KiB
TypeScript
826 lines
23 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {ApplicationRef, Injector, signal} from '@angular/core';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
|
import {compatForm, CompatValidationError} from '../../../compat/public_api';
|
|
import {
|
|
disabled,
|
|
email,
|
|
FieldState,
|
|
FieldTree,
|
|
form,
|
|
hidden,
|
|
readonly,
|
|
required,
|
|
submit,
|
|
TreeValidationResult,
|
|
validate,
|
|
validateTree,
|
|
} from '../../../public_api';
|
|
|
|
function promiseWithResolvers<T>(): {
|
|
promise: Promise<T>;
|
|
resolve: (value: T | PromiseLike<T>) => void;
|
|
reject: (reason?: any) => void;
|
|
} {
|
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
let reject!: (reason?: any) => void;
|
|
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
|
|
return {promise, resolve, reject};
|
|
}
|
|
|
|
describe('Forms compat', () => {
|
|
it('should not error on a valid value', () => {
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: new FormControl<number>(5, {nonNullable: true}),
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
const age = f.age();
|
|
expect(age.value()).toBe(5);
|
|
expect(f.age().valid()).toBe(true);
|
|
});
|
|
|
|
it('should handle and propagate errors', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.age().value()).toBe(5);
|
|
expect(f.age().valid()).toBe(true);
|
|
expect(f().valid()).toBe(true);
|
|
control.setValue(2);
|
|
expect(f.age().value()).toBe(2);
|
|
expect(f.age().valid()).toBe(false);
|
|
expect(f().valid()).toBe(false);
|
|
f.age().value.set(100);
|
|
expect(f.age().value()).toBe(100);
|
|
expect(f.age().valid()).toBe(true);
|
|
expect(f().valid()).toBe(true);
|
|
});
|
|
|
|
it('can be in root', () => {
|
|
const catControl = new FormControl('meow', Validators.minLength(3));
|
|
const cat = signal(catControl);
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f().value()).toBe('meow');
|
|
expect(f().valid()).toBe(true);
|
|
});
|
|
|
|
it('picks up the value from a new form control', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.age().value()).toBe(5);
|
|
f().value.set({age: new FormControl(10), name: 'lol'});
|
|
expect(f.age().value()).toBe(10);
|
|
|
|
const fc = new FormControl(25);
|
|
cat.set({
|
|
name: 'meow-the-cat',
|
|
age: fc,
|
|
});
|
|
expect(f.age().value()).toBe(25);
|
|
expect(f().value()).toEqual({
|
|
name: 'meow-the-cat',
|
|
age: fc,
|
|
});
|
|
});
|
|
|
|
it('handles multiple value and control changes', () => {
|
|
const control = new FormControl(100, Validators.min(3));
|
|
const cat = signal(control);
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f().value()).toBe(100);
|
|
control.setValue(101);
|
|
expect(f().value()).toBe(101);
|
|
cat.set(new FormControl(6));
|
|
expect(f().value()).toBe(6);
|
|
cat.set(new FormControl(7));
|
|
expect(f().value()).toBe(7);
|
|
cat.set(new FormControl(8));
|
|
expect(f().value()).toBe(8);
|
|
});
|
|
|
|
describe('validation', () => {
|
|
it('picks up the validation from a new form control', () => {
|
|
const control = new FormControl(5, Validators.min(10));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.age().valid()).toBeFalse();
|
|
expect(f.age().errors()).toEqual([
|
|
new CompatValidationError({control, kind: 'min', context: {min: 10, actual: 5}}),
|
|
]);
|
|
f().value.set({age: new FormControl(4), name: 'lol'});
|
|
expect(f.age().valid()).toBeTrue();
|
|
});
|
|
|
|
it('allows to manually set errors', () => {
|
|
const catControl = new FormControl('meow', Validators.minLength(3));
|
|
const cat = signal(catControl);
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
catControl.setErrors({meow: true});
|
|
expect(f().errors()).toEqual([
|
|
new CompatValidationError({kind: 'meow', context: true, control: catControl}),
|
|
]);
|
|
});
|
|
|
|
it('picks up the multiple errors from a new form control', () => {
|
|
const control = new FormControl('pirojok-the-error', [
|
|
(c) => {
|
|
return {
|
|
[c.value]: {'meow': true},
|
|
'error-1': 'error-1-content',
|
|
'error-2': 'error-2-content',
|
|
};
|
|
},
|
|
]);
|
|
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
mistake: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.mistake().valid()).toBeFalse();
|
|
expect(f.mistake().errors()).toEqual([
|
|
new CompatValidationError({kind: 'pirojok-the-error', context: {meow: true}, control}),
|
|
new CompatValidationError({kind: 'error-1', context: 'error-1-content', control}),
|
|
new CompatValidationError({kind: 'error-2', context: 'error-2-content', control}),
|
|
]);
|
|
});
|
|
|
|
it('supports async validation', async () => {
|
|
let resolve: Function = () => {};
|
|
const formControl = new FormControl<number>(5, {
|
|
nonNullable: true,
|
|
asyncValidators: () => {
|
|
// can't use promiseWithResolver here, because this runs multiple times across tests.
|
|
return new Promise<null>((r) => {
|
|
resolve = r;
|
|
});
|
|
},
|
|
});
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: formControl,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.age().pending()).toBeTrue();
|
|
|
|
resolve(null);
|
|
|
|
await TestBed.inject(ApplicationRef).whenStable();
|
|
|
|
expect(f.age().pending()).toBeFalse();
|
|
expect(f.age().valid()).toBeTrue();
|
|
|
|
f.age().control().setValue(18);
|
|
|
|
expect(f.age().pending()).toBeTrue();
|
|
expect(f.age().valid()).toBeFalse();
|
|
|
|
resolve({'incorrect-cat': 123});
|
|
|
|
await TestBed.inject(ApplicationRef).whenStable();
|
|
|
|
expect(f.age().pending()).toBeFalse();
|
|
expect(f.age().errors()).toEqual([
|
|
new CompatValidationError({kind: 'incorrect-cat', context: 123, control: formControl}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('state propagation', () => {
|
|
it('propagates disabled state from parent', () => {
|
|
const ageControl = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: ageControl,
|
|
});
|
|
|
|
const f = compatForm(
|
|
cat,
|
|
(p) => {
|
|
disabled(p, ({value}) => {
|
|
return value().name === 'disabled-cat';
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(f.name().disabled()).withContext('name is initially enabled').toBeFalse();
|
|
expect(f.age().disabled()).withContext('age is initially enabled').toBeFalse();
|
|
f.name().value.set('disabled-cat');
|
|
expect(f.name().disabled()).withContext('name is disabled').toBeTrue();
|
|
expect(f.age().disabled()).withContext('age is disabled').toBeTrue();
|
|
f.name().value.set('enabled-cat');
|
|
expect(f.name().disabled()).withContext('name is enabled').toBeFalse();
|
|
expect(f.age().disabled()).withContext('age is enabled').toBeFalse();
|
|
ageControl.disable();
|
|
expect(f.name().disabled()).withContext('name is enabled').toBeFalse();
|
|
expect(f.age().disabled()).withContext('age is disabled').toBeTrue();
|
|
});
|
|
|
|
it('propagates hidden state from parent', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat1',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(
|
|
cat,
|
|
(p) => {
|
|
hidden(p, ({value}) => {
|
|
return value().name === 'hidden-cat';
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(f.name().hidden()).withContext('name is initially displayed').toBeFalse();
|
|
expect(f.age().hidden()).withContext('age is initially displayed').toBeFalse();
|
|
f.name().value.set('hidden-cat');
|
|
expect(f.name().hidden()).withContext('name is hidden').toBeTrue();
|
|
expect(f().hidden()).withContext('name is hidden').toBeTrue();
|
|
expect(f.age().hidden()).withContext('age is hidden').toBeTrue();
|
|
});
|
|
|
|
it('propagates touched state to parent', async () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.name().touched()).withContext('name is initially not touched').toBeFalse();
|
|
expect(f().touched()).withContext('form is initially not touched').toBeFalse();
|
|
|
|
control.markAsTouched();
|
|
|
|
expect(f.age().touched()).withContext('age is touched, when control is touched').toBeTrue();
|
|
expect(f().touched()).withContext('form is touched when a child is touched').toBeTrue();
|
|
|
|
control.markAsUntouched();
|
|
|
|
expect(f.age().touched()).withContext('name is not touched when untouched').toBeFalse();
|
|
expect(f().touched()).withContext('name is not touched when child is untouched').toBeFalse();
|
|
|
|
f.age().markAsTouched();
|
|
|
|
expect(f.age().touched()).withContext('age is touched, when control is touched').toBeTrue();
|
|
expect(f().touched()).withContext('form is touched when a child is touched').toBeTrue();
|
|
});
|
|
|
|
it('picks up submittedState from parent', async () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {injector: TestBed.inject(Injector)});
|
|
|
|
expect(f().submitting()).toBe(false);
|
|
expect(f.age().submitting()).toBe(false);
|
|
|
|
const {promise, resolve} = promiseWithResolvers<TreeValidationResult>();
|
|
|
|
const result = submit(f, {
|
|
action: (field) => {
|
|
expect(field.name().value()).toBe('pirojok-the-cat');
|
|
expect(field.age().control()).toBe(control);
|
|
return promise;
|
|
},
|
|
});
|
|
|
|
expect(f().submitting()).toBe(true);
|
|
expect(f.age().submitting()).toBe(true);
|
|
|
|
resolve([]);
|
|
await result;
|
|
|
|
expect(f().submitting()).toBe(false);
|
|
expect(f.age().submitting()).toBe(false);
|
|
});
|
|
|
|
it('propagates dirty state to parent', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.age().dirty()).withContext('age is initially not dirty').toBeFalse();
|
|
expect(f().dirty()).withContext('form is initially not dirty').toBeFalse();
|
|
|
|
control.markAsDirty();
|
|
|
|
expect(f.age().dirty()).withContext('age is dirty, when control is dirty').toBeTrue();
|
|
expect(f().dirty()).withContext('form is dirty when the child is dirty').toBeTrue();
|
|
|
|
control.markAsPristine();
|
|
|
|
expect(f.age().dirty()).withContext('age is not dirty when marked as pristine').toBeFalse();
|
|
expect(f().dirty())
|
|
.withContext('age is not dirty when age is marked as pristine')
|
|
.toBeFalse();
|
|
|
|
f.age().markAsDirty();
|
|
expect(f.age().dirty()).withContext('age is dirty, when control is dirty').toBeTrue();
|
|
expect(f().dirty()).withContext('form is dirty when the child is dirty').toBeTrue();
|
|
});
|
|
|
|
it('allows resetting state', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
control.markAsDirty();
|
|
control.markAsTouched();
|
|
|
|
expect(f.age().dirty()).toBeTrue();
|
|
expect(f.age().touched()).toBeTrue();
|
|
|
|
f.age().reset();
|
|
|
|
expect(f.age().dirty()).toBeFalse();
|
|
expect(f.age().touched()).toBeFalse();
|
|
expect(f().dirty()).toBeFalse();
|
|
expect(f().touched()).toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('exposing control', () => {
|
|
it('works for control', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
address: {
|
|
house: control,
|
|
},
|
|
});
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.address.house().control()).toBe(control);
|
|
});
|
|
|
|
it('supports getting control from stateOf', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
address: {
|
|
house: control,
|
|
},
|
|
});
|
|
|
|
const f = compatForm(
|
|
cat,
|
|
(p) => {
|
|
validate(p, ({stateOf}) => {
|
|
return stateOf(p.address.house).control().value === 6 ? undefined : {kind: 'too small'};
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(f().errors()).toEqual([{kind: 'too small', fieldTree: f}]);
|
|
});
|
|
|
|
it('supports getting control from fieldTreeOf', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
address: {
|
|
house: control,
|
|
},
|
|
});
|
|
|
|
const f = compatForm(
|
|
cat,
|
|
(p) => {
|
|
validate(p, ({fieldTreeOf}) => {
|
|
return fieldTreeOf(p.address.house)().control().value === 6
|
|
? undefined
|
|
: {kind: 'too small'};
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(f().errors()).toEqual([{kind: 'too small', fieldTree: f}]);
|
|
});
|
|
|
|
it('fails for regular values', () => {
|
|
const control = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
address: {
|
|
house: control,
|
|
},
|
|
});
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
// @ts-expect-error
|
|
expect(() => f.name().control()).toThrowError();
|
|
});
|
|
});
|
|
|
|
it('disallows passing a path with a FormControl on the type level', () => {
|
|
const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]});
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
compatForm(
|
|
cat,
|
|
(path) => {
|
|
// @ts-expect-error
|
|
required(path.age);
|
|
// @ts-expect-error
|
|
validate(path.age, () => {
|
|
return undefined;
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
});
|
|
|
|
it('allows to use form control values for validation', () => {
|
|
const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]});
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
const f = compatForm(
|
|
cat,
|
|
(path) => {
|
|
required(path.name);
|
|
|
|
validate(path.name, ({valueOf}) => {
|
|
return valueOf(path.age) < 8 ? {kind: 'too small'} : undefined;
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(f.name().valid()).toBe(false);
|
|
expect(f.name().errors()).toEqual([
|
|
{
|
|
kind: 'too small',
|
|
fieldTree: f.name,
|
|
},
|
|
]);
|
|
|
|
control.setValue(10);
|
|
expect(f.name().valid()).toBe(true);
|
|
f.age().value.set(4);
|
|
expect(f.name().valid()).toBe(false);
|
|
f.age().value.update((value) => value + 6);
|
|
expect(f.name().valid()).toBe(true);
|
|
});
|
|
|
|
describe('arrays', () => {
|
|
it('works with removing an element and bringing it back', async () => {
|
|
const validCat = new FormControl('valid cat', {nonNullable: true});
|
|
const invalidCat = new FormControl('invalid cat', {
|
|
nonNullable: true,
|
|
validators: [Validators.maxLength(5)],
|
|
});
|
|
|
|
const cats = signal({
|
|
cats: [validCat, invalidCat],
|
|
});
|
|
|
|
const f = compatForm(cats, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f.cats[0]().value()).toBe('valid cat');
|
|
expect(f.cats[1]().value()).toBe('invalid cat');
|
|
|
|
expect(f.cats[0]().valid()).withContext('first cat is valid').toBe(true);
|
|
expect(f.cats[1]().valid()).withContext('second cat is not valid').toBe(false);
|
|
|
|
expect(f().valid())
|
|
.withContext('form is not valid because child validator fails')
|
|
.toBe(false);
|
|
|
|
cats.set({cats: []});
|
|
expect(f().valid())
|
|
.withContext('form is valid because invalid control has been removed')
|
|
.toBe(true);
|
|
|
|
cats.set({cats: [validCat]});
|
|
expect(f().valid())
|
|
.withContext('form is valid because only valid control was brought back')
|
|
.toBe(true);
|
|
|
|
expect(f.cats[0]().value()).toBe('valid cat');
|
|
expect(f.cats[1]).toBe(undefined);
|
|
|
|
cats.set({cats: [invalidCat]});
|
|
expect(f().valid())
|
|
.withContext('form is invalid again, because invalid child is back')
|
|
.toBe(false);
|
|
|
|
expect(f.cats[0]().value()).toBe('invalid cat');
|
|
expect(f.cats[1]).toBe(undefined);
|
|
});
|
|
});
|
|
|
|
describe('FormGroup', () => {
|
|
it('is supported', () => {
|
|
const cat = signal(
|
|
new FormGroup({
|
|
name: new FormControl('pirojok-the-cat'),
|
|
age: new FormControl(10),
|
|
}),
|
|
);
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f().value()).toEqual({
|
|
name: 'pirojok-the-cat',
|
|
age: 10,
|
|
});
|
|
|
|
expect(f().valid()).toEqual(true);
|
|
});
|
|
|
|
it('uses the actual value, not rawValue', () => {
|
|
const ageControl = new FormControl(10);
|
|
const cat = signal(
|
|
new FormGroup({
|
|
name: new FormControl('pirojok-the-cat'),
|
|
age: ageControl,
|
|
}),
|
|
);
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
ageControl.disable();
|
|
expect(f().value()).toEqual({
|
|
name: 'pirojok-the-cat',
|
|
age: 10,
|
|
});
|
|
|
|
expect(f().valid()).toEqual(true);
|
|
});
|
|
|
|
it('propagates validity and errors from child controls', () => {
|
|
const ageControl = new FormControl(1, Validators.min(5));
|
|
const cat = signal(
|
|
new FormGroup({
|
|
name: new FormControl('pirojok-the-cat'),
|
|
age: ageControl,
|
|
}),
|
|
);
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
expect(f().valid()).toEqual(false);
|
|
expect(f().errors()).toEqual([
|
|
new CompatValidationError({kind: 'min', control: ageControl, context: {min: 5, actual: 1}}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
it(`should not interpret 'FormControl' properties as subfields`, () => {
|
|
const cat = signal(new FormControl('pirojok-the-cat'));
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
// Does not pick up form control props as form children.
|
|
// @ts-expect-error
|
|
expect(f.value).toBe(undefined);
|
|
// @ts-expect-error
|
|
expect(f.setValue).toBe(undefined);
|
|
// @ts-expect-error
|
|
expect(f.child).toBe(undefined);
|
|
});
|
|
|
|
describe('type test all rules', () => {
|
|
it('compat form', () => {
|
|
const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]});
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
compatForm(
|
|
cat,
|
|
(path) => {
|
|
required(path.name);
|
|
|
|
validate(path.name, ({valueOf}) => {
|
|
return valueOf(path.age) < 8 ? {kind: 'too small'} : undefined;
|
|
});
|
|
required(path.name, {
|
|
when: ({valueOf}) => {
|
|
return valueOf(path.age) < 8;
|
|
},
|
|
});
|
|
validateTree(path.name, ({valueOf}) => {
|
|
return valueOf(path.age) < 8 ? [] : [];
|
|
});
|
|
|
|
readonly(path.name, ({valueOf}) => {
|
|
return valueOf(path.age) < 8;
|
|
});
|
|
|
|
email(path.name, {
|
|
error: ({valueOf}) => {
|
|
return valueOf(path.age) < 8 ? [] : [];
|
|
},
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect().nothing();
|
|
});
|
|
|
|
describe('regular forms', () => {
|
|
it('throws when using valueOf on FormControl', () => {
|
|
const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]});
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: control,
|
|
});
|
|
const f = form(
|
|
cat,
|
|
(p) => {
|
|
validate(p.name, ({valueOf}) => {
|
|
valueOf(p.age);
|
|
});
|
|
},
|
|
{
|
|
injector: TestBed.inject(Injector),
|
|
},
|
|
);
|
|
|
|
expect(() => f().valid()).toThrowError(/Tried to read an 'AbstractControl' value/);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('type tests', () => {
|
|
it('uses raw value', () => {
|
|
const ageControl = new FormControl(10);
|
|
const cat = signal(
|
|
new FormGroup({
|
|
name: new FormControl('pirojok-the-cat', {nonNullable: true}),
|
|
age: ageControl,
|
|
}),
|
|
);
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
const name: string = f().value().name;
|
|
});
|
|
|
|
it('unwraps the value', () => {
|
|
const ageControl = new FormControl(5, Validators.min(3));
|
|
const cat = signal({
|
|
name: 'pirojok-the-cat',
|
|
age: ageControl,
|
|
});
|
|
|
|
const f = compatForm(cat, {
|
|
injector: TestBed.inject(Injector),
|
|
});
|
|
|
|
const age: number | null = f.age().value();
|
|
// @ts-expect-error
|
|
const notAge: string = f.age().value();
|
|
});
|
|
|
|
it('allows generic CompatValidationError', () => {
|
|
const error = new CompatValidationError({
|
|
kind: 'min',
|
|
context: {min: 3, max: 4},
|
|
control: new FormControl(),
|
|
});
|
|
|
|
const min: number = error.context.min;
|
|
// @ts-expect-error
|
|
const notMin: string = error.context.min;
|
|
});
|
|
|
|
it('Allows setting T value for non compat fields', () => {
|
|
function setStateValue<T>(state: FieldState<T>, value: T) {
|
|
state.value.set(value);
|
|
}
|
|
|
|
function setValueCompatState<T>(field: FieldState<T>, value: T) {
|
|
field.value.set(field.value());
|
|
}
|
|
|
|
function setValueRegularState<T>(field: FieldState<T>, value: T) {
|
|
field.value.set(field.value());
|
|
}
|
|
|
|
function setValue<T>(f: FieldTree<T>, value: T) {
|
|
// @ts-expect-error
|
|
f().value.set(value);
|
|
}
|
|
});
|
|
});
|
|
});
|