diff --git a/packages/core/test/bundling/forms/bundle.golden_symbols.json b/packages/core/test/bundling/forms/bundle.golden_symbols.json index 829ac9a090..0e06d058e6 100644 --- a/packages/core/test/bundling/forms/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms/bundle.golden_symbols.json @@ -755,6 +755,9 @@ { "name": "classIndexOf" }, + { + "name": "cleanUpValidators" + }, { "name": "cleanUpView" }, @@ -962,6 +965,12 @@ { "name": "getConstant" }, + { + "name": "getControlAsyncValidators" + }, + { + "name": "getControlValidators" + }, { "name": "getCurrentTNode" }, @@ -1316,6 +1325,9 @@ { "name": "mergeHostAttrs" }, + { + "name": "mergeValidators" + }, { "name": "modelGroupProvider" }, @@ -1421,6 +1433,9 @@ { "name": "registerHostBindingOpCodes" }, + { + "name": "registerOnValidatorChange" + }, { "name": "registerPostOrderHooks" }, @@ -1547,6 +1562,9 @@ { "name": "setUpFormContainer" }, + { + "name": "setUpValidators" + }, { "name": "shareSubjectFactory" }, diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index f0651cfe83..050f244a76 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -9,11 +9,11 @@ import {Directive, EventEmitter, forwardRef, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges} from '@angular/core'; import {FormArray, FormControl, FormGroup} from '../../model'; -import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators'; +import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {ControlContainer} from '../control_container'; import {Form} from '../form_interface'; import {ReactiveErrors} from '../reactive_errors'; -import {cleanUpControl, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from '../shared'; +import {cleanUpControl, cleanUpValidators, removeDir, setUpControl, setUpFormContainer, setUpValidators, syncPendingControls} from '../shared'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators'; import {FormControlName} from './form_control_name'; @@ -60,8 +60,11 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan */ public readonly submitted: boolean = false; - // TODO(issue/24571): remove '!'. - private _oldForm!: FormGroup; + /** + * Reference to an old form group input value, which is needed to cleanup old instance in case it + * was replaced with a new one. + */ + private _oldForm: FormGroup|undefined; /** * @description @@ -97,6 +100,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan this._updateValidators(); this._updateDomValue(); this._updateRegistrations(); + this._oldForm = this.form; } } @@ -266,7 +270,9 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan this.directives.forEach(dir => { const newCtrl: any = this.form.get(dir.path); if (dir.control !== newCtrl) { - cleanUpControl(dir.control, dir); + // Note: the value of the `dir.control` may not be defined, for example when it's a first + // `FormControl` that is added to a `FormGroup` instance (via `addControl` call). + cleanUpControl(dir.control || null, dir); if (newCtrl) setUpControl(newCtrl, dir); (dir as {control: FormControl}).control = newCtrl; } @@ -277,14 +283,16 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan private _updateRegistrations() { this.form._registerOnCollectionChange(() => this._updateDomValue()); - if (this._oldForm) this._oldForm._registerOnCollectionChange(() => {}); - this._oldForm = this.form; + if (this._oldForm) { + this._oldForm._registerOnCollectionChange(() => {}); + } } private _updateValidators() { - this.form.validator = Validators.compose([this.form.validator, this.validator]); - this.form.asyncValidator = - Validators.composeAsync([this.form.asyncValidator, this.asyncValidator]); + setUpValidators(this.form, this, /* handleOnValidatorChange */ false); + if (this._oldForm) { + cleanUpValidators(this._oldForm, this, /* handleOnValidatorChange */ false); + } } private _checkFormPresent() { diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 3cb78e8c1b..9b7ccf545e 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -8,8 +8,8 @@ import {isDevMode} from '@angular/core'; -import {FormArray, FormControl, FormGroup} from '../model'; -import {Validators} from '../validators'; +import {AbstractControl, FormArray, FormControl, FormGroup} from '../model'; +import {getControlAsyncValidators, getControlValidators, mergeValidators} from '../validators'; import {AbstractControlDirective} from './abstract_control_directive'; import {AbstractFormGroupDirective} from './abstract_form_group_directive'; @@ -25,7 +25,7 @@ import {FormArrayName} from './reactive_directives/form_group_name'; import {ReactiveErrors} from './reactive_errors'; import {SelectControlValueAccessor} from './select_control_value_accessor'; import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor'; -import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators'; +import {AsyncValidatorFn, Validator, ValidatorFn} from './validators'; export function controlPath(name: string|null, parent: ControlContainer): string[] { @@ -38,8 +38,8 @@ export function setUpControl(control: FormControl, dir: NgControl): void { if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with'); } - control.validator = Validators.compose([control.validator!, dir.validator]); - control.asyncValidator = Validators.composeAsync([control.asyncValidator!, dir.asyncValidator]); + setUpValidators(control, dir, /* handleOnValidatorChange */ true); + dir.valueAccessor!.writeValue(control.value); setUpViewChangePipeline(control, dir); @@ -52,20 +52,9 @@ export function setUpControl(control: FormControl, dir: NgControl): void { dir.valueAccessor!.setDisabledState!(isDisabled); }); } - - // re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4 - dir._rawValidators.forEach((validator: Validator|ValidatorFn) => { - if ((validator).registerOnValidatorChange) - (validator).registerOnValidatorChange!(() => control.updateValueAndValidity()); - }); - - dir._rawAsyncValidators.forEach((validator: AsyncValidator|AsyncValidatorFn) => { - if ((validator).registerOnValidatorChange) - (validator).registerOnValidatorChange!(() => control.updateValueAndValidity()); - }); } -export function cleanUpControl(control: FormControl, dir: NgControl) { +export function cleanUpControl(control: FormControl|null, dir: NgControl) { const noop = () => { if (typeof ngDevMode === 'undefined' || ngDevMode) { _noControlError(dir); @@ -75,21 +64,100 @@ export function cleanUpControl(control: FormControl, dir: NgControl) { dir.valueAccessor!.registerOnChange(noop); dir.valueAccessor!.registerOnTouched(noop); - dir._rawValidators.forEach((validator: any) => { - if (validator.registerOnValidatorChange) { - validator.registerOnValidatorChange(null); - } - }); - - dir._rawAsyncValidators.forEach((validator: any) => { - if (validator.registerOnValidatorChange) { - validator.registerOnValidatorChange(null); - } - }); + cleanUpValidators(control, dir, /* handleOnValidatorChange */ true); if (control) control._clearChangeFns(); } +function registerOnValidatorChange(validators: (V|Validator)[], onChange: () => void): void { + validators.forEach((validator: (V|Validator)) => { + if ((validator).registerOnValidatorChange) + (validator).registerOnValidatorChange!(onChange); + }); +} + +/** + * Sets up sync and async directive validators on provided form control. + * This function merges validators from the directive into the validators of the control. + * + * @param control Form control where directive validators should be setup. + * @param dir Directive instance that contains validators to be setup. + * @param handleOnValidatorChange Flag that determines whether directive validators should be setup + * to handle validator input change. + */ +export function setUpValidators( + control: AbstractControl, dir: AbstractControlDirective, + handleOnValidatorChange: boolean): void { + const validators = getControlValidators(control); + if (dir.validator !== null) { + control.setValidators(mergeValidators(validators, dir.validator)); + } else if (typeof validators === 'function') { + // If sync validators are represented by a single validator function, we force the + // `Validators.compose` call to happen by executing the `setValidators` function with + // an array that contains that function. We need this to avoid possible discrepancies in + // validators behavior, so sync validators are always processed by the `Validators.compose`. + // Note: we should consider moving this logic inside the `setValidators` function itself, so we + // have consistent behavior on AbstractControl API level. The same applies to the async + // validators logic below. + control.setValidators([validators]); + } + + const asyncValidators = getControlAsyncValidators(control); + if (dir.asyncValidator !== null) { + control.setAsyncValidators( + mergeValidators(asyncValidators, dir.asyncValidator)); + } else if (typeof asyncValidators === 'function') { + control.setAsyncValidators([asyncValidators]); + } + + // Re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4 + if (handleOnValidatorChange) { + const onValidatorChange = () => control.updateValueAndValidity(); + registerOnValidatorChange(dir._rawValidators, onValidatorChange); + registerOnValidatorChange(dir._rawAsyncValidators, onValidatorChange); + } +} + +/** + * Cleans up sync and async directive validators on provided form control. + * This function reverts the setup performed by the `setUpValidators` function, i.e. + * removes directive-specific validators from a given control instance. + * + * @param control Form control from where directive validators should be removed. + * @param dir Directive instance that contains validators to be removed. + * @param handleOnValidatorChange Flag that determines whether directive validators should also be + * cleaned up to stop handling validator input change (if previously configured to do so). + */ +export function cleanUpValidators( + control: AbstractControl|null, dir: AbstractControlDirective, + handleOnValidatorChange: boolean): void { + if (control !== null) { + if (dir.validator !== null) { + const validators = getControlValidators(control); + if (Array.isArray(validators) && validators.length > 0) { + // Filter out directive validator function. + control.setValidators(validators.filter(validator => validator !== dir.validator)); + } + } + + if (dir.asyncValidator !== null) { + const asyncValidators = getControlAsyncValidators(control); + if (Array.isArray(asyncValidators) && asyncValidators.length > 0) { + // Filter out directive async validator function. + control.setAsyncValidators( + asyncValidators.filter(asyncValidator => asyncValidator !== dir.asyncValidator)); + } + } + } + + if (handleOnValidatorChange) { + // Clear onValidatorChange callbacks by providing a noop function. + const noop = () => {}; + registerOnValidatorChange(dir._rawValidators, noop); + registerOnValidatorChange(dir._rawAsyncValidators, noop); + } +} + function setUpViewChangePipeline(control: FormControl, dir: NgControl): void { dir.valueAccessor!.registerOnChange((newValue: any) => { control._pendingValue = newValue; @@ -130,8 +198,7 @@ export function setUpFormContainer( control: FormGroup|FormArray, dir: AbstractFormGroupDirective|FormArrayName) { if (control == null && (typeof ngDevMode === 'undefined' || ngDevMode)) _throwError(dir, 'Cannot find control with'); - control.validator = Validators.compose([control.validator, dir.validator]); - control.asyncValidator = Validators.composeAsync([control.asyncValidator, dir.asyncValidator]); + setUpValidators(control, dir, /* handleOnValidatorChange */ false); } function _noControlError(dir: NgControl) { diff --git a/packages/forms/src/validators.ts b/packages/forms/src/validators.ts index 5ebf86ce7a..5e32394edf 100644 --- a/packages/forms/src/validators.ts +++ b/packages/forms/src/validators.ts @@ -551,4 +551,29 @@ export function composeAsyncValidators(validators: Array(validators)) : null; +} + +/** + * Merges raw control validators with a given directive validator and returns the combined list of + * validators as an array. + */ +export function mergeValidators(controlValidators: V|V[]|null, dirValidator: V): V[] { + if (controlValidators === null) return [dirValidator]; + return Array.isArray(controlValidators) ? [...controlValidators, dirValidator] : + [controlValidators, dirValidator]; +} + +/** + * Retrieves the list of raw synchronous validators attached to a given control. + */ +export function getControlValidators(control: AbstractControl): ValidatorFn|ValidatorFn[]|null { + return (control as any)._rawValidators as ValidatorFn | ValidatorFn[] | null; +} + +/** + * Retrieves the list of raw asynchronous validators attached to a given control. + */ +export function getControlAsyncValidators(control: AbstractControl): AsyncValidatorFn| + AsyncValidatorFn[]|null { + return (control as any)._rawAsyncValidators as AsyncValidatorFn | AsyncValidatorFn[] | null; } \ No newline at end of file diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index b6e3fb3740..170ed7bb6c 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -9,11 +9,12 @@ import {ɵgetDOM as getDOM} from '@angular/common'; import {Component, Directive, forwardRef, Input, Type} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ReactiveFormsModule, Validators} from '@angular/forms'; +import {expect} from '@angular/core/testing/src/testing_internal'; +import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ReactiveFormsModule, Validator, Validators} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {dispatchEvent, sortedClassList} from '@angular/platform-browser/testing/src/browser_util'; -import {merge, timer} from 'rxjs'; -import {tap} from 'rxjs/operators'; +import {merge, NEVER, of, timer} from 'rxjs'; +import {map, tap} from 'rxjs/operators'; import {MyInput, MyInputForm} from './value_accessor_integration_spec'; @@ -25,6 +26,11 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; return TestBed.createComponent(component); } + // Helper method that attaches a spy to a `validate` function on a Validator class. + function validatorSpyOn(validatorClass: any) { + return spyOn(validatorClass.prototype, 'validate').and.callThrough(); + } + describe('basic functionality', () => { it('should work with single controls', () => { const fixture = initTest(FormControlComp); @@ -1785,6 +1791,23 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(control.hasError('required')).toEqual(false); }); + // Note: this scenario goes against validator function rules were `null` is the only + // representation of a successful check. However the `Validators.combine` has a side-effect + // where falsy values are treated as success and `null` is returned from the wrapper function. + // The goal of this test is to prevent regressions for validators that return falsy values by + // mistake and rely on the `Validators.compose` side-effects to normalize the value to `null` + // instead. + it('should treat validators that return `undefined` as successful', () => { + const fixture = initTest(FormControlComp); + const validatorFn = (control: AbstractControl) => control.value ?? undefined; + const control = new FormControl(undefined, validatorFn); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + expect(control.status).toBe('VALID'); + expect(control.errors).toBe(null); + }); + it('should use sync validators defined in html', () => { const fixture = initTest(LoginIsEmptyWrapper, LoginIsEmptyValidator); const form = new FormGroup({ @@ -2446,6 +2469,181 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(fixture.componentInstance.control.value).toEqual('updatedValue'); }); }); + + describe('cleanup', () => { + function expectValidatorsToBeCalled( + syncValidatorSpy: jasmine.Spy, asyncValidatorSpy: jasmine.Spy, + expected: {ctx: any, count: number}) { + [syncValidatorSpy, asyncValidatorSpy].forEach((spy: jasmine.Spy) => { + spy.calls.all().forEach((call: jasmine.CallInfo) => { + expect(call.args[0]).toBe(expected.ctx); + }); + expect(spy).toHaveBeenCalledTimes(expected.count); + }); + } + + it('should clean up validators when FormGroup is replaced', () => { + const fixture = + initTest(FormGroupWithValidators, MyCustomValidator, MyCustomAsyncValidator); + fixture.detectChanges(); + + const newForm = new FormGroup({login: new FormControl('NEW')}); + const oldForm = fixture.componentInstance.form; + + // Update `form` input with a new value. + fixture.componentInstance.form = newForm; + fixture.detectChanges(); + + const validatorSpy = validatorSpyOn(MyCustomValidator); + const asyncValidatorSpy = validatorSpyOn(MyCustomAsyncValidator); + + // Calling `setValue` for the OLD form should NOT trigger validator calls. + oldForm.setValue({login: 'SOME-OLD-VALUE'}); + expect(validatorSpy).not.toHaveBeenCalled(); + expect(asyncValidatorSpy).not.toHaveBeenCalled(); + + // Calling `setValue` for the NEW (active) form should trigger validator calls. + newForm.setValue({login: 'SOME-NEW-VALUE'}); + expectValidatorsToBeCalled(validatorSpy, asyncValidatorSpy, {ctx: newForm, count: 1}); + }); + + it('should clean up validators when FormControl inside FormGroup is replaced', () => { + const fixture = + initTest(FormControlWithValidators, MyCustomValidator, MyCustomAsyncValidator); + fixture.detectChanges(); + + const newControl = new FormControl('NEW')!; + const oldControl = fixture.componentInstance.form.get('login')!; + + const validatorSpy = validatorSpyOn(MyCustomValidator); + const asyncValidatorSpy = validatorSpyOn(MyCustomAsyncValidator); + + // Update `login` form control with a new `FormControl` instance. + fixture.componentInstance.form.removeControl('login'); + fixture.componentInstance.form.addControl('login', newControl); + fixture.detectChanges(); + + validatorSpy.calls.reset(); + asyncValidatorSpy.calls.reset(); + + // Calling `setValue` for the OLD control should NOT trigger validator calls. + oldControl.setValue('SOME-OLD-VALUE'); + expect(validatorSpy).not.toHaveBeenCalled(); + expect(asyncValidatorSpy).not.toHaveBeenCalled(); + + // Calling `setValue` for the NEW (active) control should trigger validator calls. + newControl.setValue('SOME-NEW-VALUE'); + expectValidatorsToBeCalled(validatorSpy, asyncValidatorSpy, {ctx: newControl, count: 1}); + }); + + it('should keep control in pending state if async validator never emits', fakeAsync(() => { + const fixture = initTest(FormControlWithAsyncValidatorFn); + fixture.detectChanges(); + + const control = fixture.componentInstance.form.get('login')!; + expect(control.status).toBe('PENDING'); + + control.setValue('SOME-NEW-VALUE'); + tick(); + + // Since validator never emits, we expect a control to be retained in a pending state. + expect(control.status).toBe('PENDING'); + expect(control.errors).toBe(null); + })); + + it('should call validators defined via `set[Async]Validators` after view init', () => { + const fixture = + initTest(FormControlWithValidators, MyCustomValidator, MyCustomAsyncValidator); + fixture.detectChanges(); + + const control = fixture.componentInstance.form.get('login')!; + + const initialValidatorSpy = validatorSpyOn(MyCustomValidator); + const initialAsyncValidatorSpy = validatorSpyOn(MyCustomAsyncValidator); + + initialValidatorSpy.calls.reset(); + initialAsyncValidatorSpy.calls.reset(); + + control.setValue('VALUE-A'); + + // Expect initial validators (setup during view creation) to be called. + expectValidatorsToBeCalled( + initialValidatorSpy, initialAsyncValidatorSpy, {ctx: control, count: 1}); + + initialValidatorSpy.calls.reset(); + initialAsyncValidatorSpy.calls.reset(); + + // Create new validators and corresponding spies. + const newValidatorSpy = jasmine.createSpy('newValidator').and.returnValue(null); + const newAsyncValidatorSpy = + jasmine.createSpy('newAsyncValidator').and.returnValue(of(null)); + + // Set new validators to a control that is already used in a view. + // Expect that new validators are applied and old validators are removed. + control.setValidators(newValidatorSpy); + control.setAsyncValidators(newAsyncValidatorSpy); + + // Update control value to trigger validation. + control.setValue('VALUE-B'); + + // Verify that initial (inactive) validators were not called. + expect(initialValidatorSpy).not.toHaveBeenCalled(); + expect(initialAsyncValidatorSpy).not.toHaveBeenCalled(); + + // Verify that newly applied validators were executed. + expectValidatorsToBeCalled(newValidatorSpy, newAsyncValidatorSpy, {ctx: control, count: 1}); + }); + + it('should cleanup validators on a control used for multiple `formControlName` directive', + () => { + const fixture = + initTest(NgForFormControlWithValidators, MyCustomValidator, MyCustomAsyncValidator); + fixture.detectChanges(); + + const newControl = new FormControl('b')!; + const oldControl = fixture.componentInstance.form.get('login')!; + + const validatorSpy = validatorSpyOn(MyCustomValidator); + const asyncValidatorSpy = validatorSpyOn(MyCustomAsyncValidator); + + // Case 1: replace `login` form control with a new `FormControl` instance. + fixture.componentInstance.form.removeControl('login'); + fixture.componentInstance.form.addControl('login', newControl); + fixture.detectChanges(); + + // Check that validators were called with a new control as a context + // and each validator function was called for each control (so 3 times each). + expectValidatorsToBeCalled(validatorSpy, asyncValidatorSpy, {ctx: newControl, count: 3}); + + validatorSpy.calls.reset(); + asyncValidatorSpy.calls.reset(); + + // Calling `setValue` for the OLD control should NOT trigger validator calls. + oldControl.setValue('SOME-OLD-VALUE'); + expect(validatorSpy).not.toHaveBeenCalled(); + expect(asyncValidatorSpy).not.toHaveBeenCalled(); + + // Calling `setValue` for the NEW (active) control should trigger validator calls. + newControl.setValue('SOME-NEW-VALUE'); + + // Check that setting a value on a new control triggers validator calls. + expectValidatorsToBeCalled(validatorSpy, asyncValidatorSpy, {ctx: newControl, count: 3}); + + // Case 2: update `logins` to render a new list of elements. + fixture.componentInstance.logins = ['a', 'b', 'c', 'd', 'e', 'f']; + fixture.detectChanges(); + + validatorSpy.calls.reset(); + asyncValidatorSpy.calls.reset(); + + // Calling `setValue` for the NEW (active) control should trigger validator calls. + newControl.setValue('SOME-NEW-VALUE-2'); + + // Check that setting a value on a new control triggers validator calls for updated set + // of controls (one for each element in the `logins` array). + expectValidatorsToBeCalled(validatorSpy, asyncValidatorSpy, {ctx: newControl, count: 6}); + }); + }); }); } @@ -2678,3 +2876,89 @@ class UniqLoginWrapper { // TODO(issue/24571): remove '!'. form!: FormGroup; } + +@Component({ + selector: 'form-group-with-validators', + template: ` +
+ +
+ ` +}) +class FormGroupWithValidators { + form = new FormGroup({login: new FormControl('INITIAL')}); +} + +@Component({ + selector: 'form-control-with-validators', + template: ` +
+ +
+ ` +}) +class FormControlWithAsyncValidatorFn { + control = new FormControl('INITIAL'); + form = new FormGroup({login: this.control}); + + constructor() { + this.control.setAsyncValidators(() => { + return NEVER.pipe(map((_: any) => ({timeoutError: true}))); + }); + } +} + +@Component({ + selector: 'form-control-with-validators', + template: ` +
+ +
+ ` +}) +class FormControlWithValidators { + form = new FormGroup({login: new FormControl('INITIAL')}); +} + +@Component({ + selector: 'ngfor-form-controls-with-validators', + template: ` +
+ + + +
+ ` +}) +class NgForFormControlWithValidators { + form = new FormGroup({login: new FormControl('a')}); + logins = ['a', 'b', 'c']; +} + +@Directive({ + selector: '[my-custom-validator]', + providers: [{ + provide: NG_VALIDATORS, + useClass: forwardRef(() => MyCustomValidator), + multi: true, + }] +}) +class MyCustomValidator implements Validator { + validate(control: AbstractControl) { + return null; + } +} + +@Directive({ + selector: '[my-custom-async-validator]', + providers: [{ + provide: NG_ASYNC_VALIDATORS, + useClass: forwardRef(() => MyCustomAsyncValidator), + multi: true, + }] +}) +class MyCustomAsyncValidator implements AsyncValidator { + validate(control: AbstractControl) { + return Promise.resolve(null); + } +} \ No newline at end of file