angular-reactive-validation
Version:
Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy.
457 lines (444 loc) • 22.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, Component, ViewEncapsulation, Optional, Input, InjectionToken, Inject, ContentChildren, NgModule } from '@angular/core';
import * as i1 from '@angular/forms';
import { UntypedFormControl, Validators as Validators$1 } from '@angular/forms';
import * as i3 from '@angular/common';
import { CommonModule } from '@angular/common';
import { Subscription } from 'rxjs';
/**
* Encapsulates properties and events of the form and makes them available for child components.
*/
class FormDirective {
constructor(formGroupDirective) {
this.submitted = formGroupDirective.ngSubmit.asObservable();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: FormDirective, deps: [{ token: i1.FormGroupDirective }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.2", type: FormDirective, selector: "form[formGroup]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: FormDirective, decorators: [{
type: Directive,
args: [{
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'form[formGroup]'
}]
}], ctorParameters: () => [{ type: i1.FormGroupDirective }] });
const getFormControlFromContainer = (name, controlContainer) => {
if (controlContainer) {
const control = controlContainer.control.controls[name];
if (!control) {
throw new Error(`There is no control named '${name}'` +
(getPath(controlContainer).length > 0 ? ` within '${getPath(controlContainer).join('.')}'` : '') + '.');
}
if (!(control instanceof UntypedFormControl)) {
throw new Error(`The control named '${name}' ` +
(getPath(controlContainer).length > 0 ? `within '${getPath(controlContainer).join('.')}' ` : '') +
`is not a FormControl. Maybe you accidentally referenced a FormGroup or FormArray?`);
}
return control;
}
else {
throw new Error(`You can't pass a string to arv-validation-messages's for attribute, when the ` +
`arv-validation-messages element is not a child of an element with a formGroupName or formGroup declaration.`);
}
};
const isControlContainerVoidOrInitialized = (controlContainer) => !!(!controlContainer || controlContainer.form ||
(controlContainer.formDirective && controlContainer.formDirective.form));
const getPath = (controlContainer) => controlContainer.path || [];
/**
* The ValidationMessageComponent lets the developer specify a custom visual style and custom error message
* for edge-cases where the standard style or message capabilities do not suffice.
*
* TODO: Trigger revalidation by parent whenever [for] changes.
*/
class ValidationMessageComponent {
set for(control) {
if (!isControlContainerVoidOrInitialized(this.controlContainer)) {
this.initializeForOnInit = () => this.for = control;
return;
}
this._for = typeof control === 'string' ? getFormControlFromContainer(control, this.controlContainer) : control;
}
get for() {
return this._for;
}
constructor(controlContainer) {
this.controlContainer = controlContainer;
this.initializeForOnInit = () => { };
}
ngOnInit() {
this.initializeForOnInit();
}
canHandle(error) {
return (!this.for || error.control === this.for) && error.key === this.key;
}
show(error) {
this._context = error.errorObject;
}
reset() {
this._context = undefined;
}
/**
* The ValidationErrors object that contains contextual information about the error, which can be used for
* displaying, e.g. the minimum length within the error message.
*/
get context() {
return this._context;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ValidationMessageComponent, deps: [{ token: i1.ControlContainer, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.2", type: ValidationMessageComponent, selector: "arv-validation-message", inputs: { for: "for", key: "key" }, ngImport: i0, template: "<div *ngIf=\"context\">\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ValidationMessageComponent, decorators: [{
type: Component,
args: [{ selector: 'arv-validation-message', encapsulation: ViewEncapsulation.None, template: "<div *ngIf=\"context\">\n <ng-content></ng-content>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1.ControlContainer, decorators: [{
type: Optional
}] }], propDecorators: { for: [{
type: Input
}],
/**
* The name of the returned validation object property for which the custom validation message should be shown.
*/
key: [{
type: Input
}] } });
class ValidationError {
constructor(control, key, errorObject) {
this.control = control;
this.key = key;
this.errorObject = errorObject;
}
static fromFirstError(control) {
if (!control.errors) {
return undefined;
}
return new ValidationError(control, Object.keys(control.errors)[0], control.errors[Object.keys(control.errors)[0]]);
}
hasMessage() {
return !!this.getMessage();
}
getMessage() {
return this.errorObject['message'];
}
}
const REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN = new InjectionToken('ReactiveValidationModuleConfiguration');
/**
* Given a control, returns a string representation of the property path to
* this control. Thus, for a FormControl 'firstName', that is part of a
* FormGroup named 'name', this function will return: 'name.firstName'.
*
* Note that FormArray indexes are also put in the path, e.g.: 'person.0.name.firstName'.
*/
const getControlPath = (control) => {
const parentControl = control.parent;
if (parentControl) {
let path = getControlPath(parentControl);
if (path) {
path += '.';
}
return path + Object.keys(parentControl.controls).find(key => {
const controls = parentControl.controls;
if (Array.isArray(controls)) {
return controls[Number(key)] === control;
}
else {
return controls[key] === control;
}
});
}
return '';
};
/**
* The ValidationMessagesComponent shows validation messages for one to many FormControls. It either shows
* messages specified within the reactive form model, or shows custom messages declared using the
* ValidationMessageComponent.
*/
class ValidationMessagesComponent {
constructor(controlContainer, formSubmitDirective, configuration) {
this.controlContainer = controlContainer;
this.configuration = configuration;
this._for = [];
this.messageComponentsChangesSubscription = new Subscription();
this.controlStatusChangesSubscription = new Subscription();
this.formSubmitted = undefined;
this.formSubmittedSubscription = new Subscription();
this.initializeForOnInit = () => { };
if (formSubmitDirective) {
this.formSubmitted = false;
this.formSubmittedSubscription.add(formSubmitDirective.submitted.subscribe(() => {
this.formSubmitted = true;
}));
}
}
ngOnInit() {
this.initializeForOnInit();
}
ngAfterContentInit() {
this.messageComponentsChangesSubscription.add(this.messageComponents?.changes.subscribe(this.validateChildren));
this.validateChildren();
this._for.forEach(control => {
this.handleControlStatusChange(control);
});
}
ngOnDestroy() {
this.messageComponentsChangesSubscription.unsubscribe();
this.formSubmittedSubscription.unsubscribe();
this.controlStatusChangesSubscription.unsubscribe();
}
isValid() {
return this.getFirstErrorPerControl().length === 0;
}
getErrorMessages() {
return this.getFirstErrorPerControl().filter(error => error.hasMessage())
.map(error => error.getMessage());
}
set for(controls) {
if (!isControlContainerVoidOrInitialized(this.controlContainer)) {
this.initializeForOnInit = () => this.for = controls;
return;
}
if (!Array.isArray(controls)) {
controls = controls !== undefined ? [controls] : [];
}
if (controls.length === 0) {
throw new Error(`arv-validation-messages doesn't allow declaring an empty array as input to the 'for' attribute.`);
}
this._for = controls.map(control => typeof control === 'string' ?
getFormControlFromContainer(control, this.controlContainer) : control);
this.validateChildren();
this.controlStatusChangesSubscription.unsubscribe();
this.controlStatusChangesSubscription = new Subscription();
this._for.forEach(control => {
this.controlStatusChangesSubscription.add(control.statusChanges.subscribe(() => {
this.handleControlStatusChange(control);
}));
});
}
getFirstErrorPerControl() {
return this._for.filter(control => this.configuration && this.configuration.displayValidationMessageWhen ?
this.configuration.displayValidationMessageWhen(control, this.formSubmitted) : control.touched || this.formSubmitted).map(ValidationError.fromFirstError).filter(value => value !== undefined);
}
/**
* Validates that the child ValidationMessageComponents declare what FormControl they specify a message for (when needed); and
* that the declared FormControl is actually part of the parent ValidationMessagesComponent 'for' collection (when specified).
*/
validateChildren() {
if (!this.messageComponents) {
return;
}
this.messageComponents.forEach(component => {
if (this._for.length > 1 && component.for === undefined) {
throw new Error(`Specify the FormControl for which the arv-validation-message element with key '${component.key}' ` +
`should show messages.`);
}
if (component.for && this._for.indexOf(component.for) === -1) {
throw new Error(`A arv-validation-messages element with key '${component.key}' attempts to show messages ` +
`for a FormControl that is not declared in the parent arv-validation-messages element.`);
}
});
}
handleControlStatusChange(control) {
if (!this.messageComponents) {
return;
}
this.messageComponents.filter(component => component.for === control || component.for === undefined)
.forEach(component => component.reset());
const error = ValidationError.fromFirstError(control);
if (!error || error.hasMessage()) {
return;
}
const messageComponent = this.messageComponents.find(component => component.canHandle(error));
if (messageComponent) {
messageComponent.show(error);
}
else {
throw new Error(`There is no suitable arv-validation-message element to show the '${error.key}' ` +
`error of '${getControlPath(error.control)}'`);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ValidationMessagesComponent, deps: [{ token: i1.ControlContainer, optional: true }, { token: FormDirective, optional: true }, { token: REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.2", type: ValidationMessagesComponent, selector: "arv-validation-messages", inputs: { for: "for" }, queries: [{ propertyName: "messageComponents", predicate: ValidationMessageComponent }], ngImport: i0, template: "<div *ngIf=\"!isValid()\">\n <div class=\"invalid-feedback\">\n <p *ngFor=\"let message of getErrorMessages()\">{{message}}</p>\n </div>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ValidationMessagesComponent, decorators: [{
type: Component,
args: [{ selector: 'arv-validation-messages', encapsulation: ViewEncapsulation.None, template: "<div *ngIf=\"!isValid()\">\n <div class=\"invalid-feedback\">\n <p *ngFor=\"let message of getErrorMessages()\">{{message}}</p>\n </div>\n <ng-content></ng-content>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1.ControlContainer, decorators: [{
type: Optional
}] }, { type: FormDirective, decorators: [{
type: Optional
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN]
}] }], propDecorators: { messageComponents: [{
type: ContentChildren,
args: [ValidationMessageComponent]
}], for: [{
type: Input
}] } });
class ReactiveValidationModule {
static forRoot(configuration) {
return {
ngModule: ReactiveValidationModule,
providers: [{
provide: REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN, useValue: configuration
}]
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ReactiveValidationModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.2", ngImport: i0, type: ReactiveValidationModule, declarations: [ValidationMessagesComponent,
ValidationMessageComponent,
FormDirective], imports: [CommonModule], exports: [ValidationMessagesComponent,
ValidationMessageComponent,
FormDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ReactiveValidationModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: ReactiveValidationModule, decorators: [{
type: NgModule,
args: [{
imports: [
CommonModule
],
declarations: [
ValidationMessagesComponent,
ValidationMessageComponent,
FormDirective
],
exports: [
ValidationMessagesComponent,
ValidationMessageComponent,
FormDirective
]
}]
}] });
/**
* @dynamic
*/
class ValidatorDeclaration {
/**
* Wraps your own validator functions for use with the angular-reactive-validation library.
*
* @param validatorFn A function you want to wrap which can validate a control.
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
*/
static wrapNoArgumentValidator(validatorFn, resultKey) {
return (message) => (control) => ValidatorDeclaration.validateAndSetMessageIfInvalid(control, () => validatorFn, resultKey, message);
}
/**
* Wraps your own validator functions for use with the angular-reactive-validation library.
*
* @param validatorFactoryFn A function which accepts a single argument and returns a ValidatorFn.
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
*/
static wrapSingleArgumentValidator(validatorFactoryFn, resultKey) {
return (arg1, message) => (control) => {
const unwrappedArg1 = ValidatorDeclaration.unwrapArgument(arg1);
return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, unwrappedArg1);
};
}
/**
* Wraps your own validator functions for use with the angular-reactive-validation library.
*
* @param validatorFactoryFn A function which accepts two arguments and returns a ValidatorFn.
* @param resultKey The error key used for indicating an error result as returned from the ValidatorFn.
*/
static wrapTwoArgumentValidator(validatorFactoryFn, resultKey) {
return (arg1, arg2, message) => (control) => {
const unwrappedArg1 = ValidatorDeclaration.unwrapArgument(arg1);
const unwrappedArg2 = ValidatorDeclaration.unwrapArgument(arg2);
return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, unwrappedArg1, unwrappedArg2);
};
}
static unwrapArgument(arg) {
if (arg instanceof Function) {
arg = arg();
}
return arg;
}
static validateAndSetMessageIfInvalid(control,
// eslint-disable-next-line @typescript-eslint/no-shadow
validatorFactoryFn, resultKey,
// eslint-disable-next-line @typescript-eslint/no-shadow
message, ...args) {
const validationResult = ValidatorDeclaration.validate(control, validatorFactoryFn, ...args);
ValidatorDeclaration.setMessageIfInvalid(control, resultKey, validationResult, message, ...args);
return validationResult;
}
// eslint-disable-next-line @typescript-eslint/no-shadow
static validate(control, validatorFactoryFn, ...args) {
const wrappedValidatorFn = validatorFactoryFn(...args);
return wrappedValidatorFn(control);
}
static setMessageIfInvalid(control, resultKey,
// eslint-disable-next-line @typescript-eslint/no-shadow
validationResult, message, ...args) {
if (message) {
if (validationResult && validationResult[resultKey]) {
if (typeof message === 'function') {
message = message(...args);
}
// Not all validators set an object. Often they'll simply set a property to true.
// Here, we replace any non-object (or array) to be an object on which we can set a message.
if (!ValidatorDeclaration.isObject(validationResult[resultKey])) {
validationResult[resultKey] = {};
}
validationResult[resultKey]['message'] = message;
}
}
}
static isObject(arg) {
return arg !== null && typeof arg === 'object' && !Array.isArray(arg);
}
}
/**
* Provides a set of validators used by form controls.
*
* Code comments have been copied from the Angular source code.
*/
class Validators {
/**
* No-op validator.
*/
static { this.nullValidator = Validators$1.nullValidator; }
static { this.composeAsync = Validators$1.composeAsync; }
static { this.minValidator = ValidatorDeclaration.wrapSingleArgumentValidator(Validators$1.min, 'min'); }
static { this.maxValidator = ValidatorDeclaration.wrapSingleArgumentValidator(Validators$1.max, 'max'); }
static { this.minLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(Validators$1.minLength, 'minlength'); }
static { this.maxLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(Validators$1.maxLength, 'maxlength'); }
static { this.patternValidator = ValidatorDeclaration.wrapSingleArgumentValidator(Validators$1.pattern, 'pattern'); }
static { this.requiredValidator = ValidatorDeclaration.wrapNoArgumentValidator(Validators$1.required, 'required'); }
static { this.requiredTrueValidator = ValidatorDeclaration.wrapNoArgumentValidator(Validators$1.requiredTrue, 'required'); }
static { this.emailValidator = ValidatorDeclaration.wrapNoArgumentValidator(Validators$1.email, 'email'); }
static compose(validators) {
return validators === null ? Validators$1.compose(validators) : Validators$1.compose(validators);
}
static min(min, message) {
return Validators.minValidator(min, message);
}
static max(max, message) {
return Validators.maxValidator(max, message);
}
static minLength(minLength, message) {
return Validators.minLengthValidator(minLength, message);
}
static maxLength(maxLength, message) {
return Validators.maxLengthValidator(maxLength, message);
}
static pattern(pattern, message) {
return Validators.patternValidator(pattern, message);
}
static required(message) {
return Validators.requiredValidator(message);
}
static requiredTrue(message) {
return Validators.requiredTrueValidator(message);
}
static email(message) {
return Validators.emailValidator(message);
}
}
/**
* Generated bundle index. Do not edit.
*/
export { FormDirective, ReactiveValidationModule, ValidationMessageComponent, ValidationMessagesComponent, ValidatorDeclaration, Validators };
//# sourceMappingURL=angular-reactive-validation.mjs.map