ngx-vest-forms
Version:
Opinionated template-driven forms library for Angular
703 lines (689 loc) • 30.8 kB
JavaScript
import * as i0 from '@angular/core';
import { input, Directive, isDevMode, inject, Output, ChangeDetectorRef, Component, ChangeDetectionStrategy, ContentChild, HostBinding, Optional } from '@angular/core';
import { FormGroup, FormArray, NG_ASYNC_VALIDATORS, NgForm, StatusChangeEvent, ValueChangeEvent, PristineChangeEvent, NgModelGroup, NgModel, ControlContainer, FormsModule } from '@angular/forms';
import { Subject, of, ReplaySubject, debounceTime, take, switchMap, Observable, takeUntil, filter, map, distinctUntilChanged, startWith, tap, zip, mergeWith } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
const ROOT_FORM = 'rootForm';
/**
* Recursively calculates the path of a form control
* @param formGroup
* @param control
*/
function getControlPath(formGroup, control) {
for (const key in formGroup.controls) {
if (formGroup.controls.hasOwnProperty(key)) {
const ctrl = formGroup.get(key);
if (ctrl instanceof FormGroup) {
const path = getControlPath(ctrl, control);
if (path) {
return key + '.' + path;
}
}
else if (ctrl === control) {
return key;
}
}
}
return '';
}
/**
* Recursively calculates the path of a form group
* @param formGroup
* @param control
*/
function getGroupPath(formGroup, control) {
for (const key in formGroup.controls) {
if (formGroup.controls.hasOwnProperty(key)) {
const ctrl = formGroup.get(key);
if (ctrl === control) {
return key;
}
if (ctrl instanceof FormGroup) {
const path = getGroupPath(ctrl, control);
if (path) {
return key + '.' + path;
}
}
}
}
return '';
}
/**
* Calculates the field name of a form control: Eg: addresses.shippingAddress.street
* @param rootForm
* @param control
*/
function getFormControlField(rootForm, control) {
return getControlPath(rootForm, control);
}
/**
* Calcuates the field name of a form group Eg: addresses.shippingAddress
* @param rootForm
* @param control
*/
function getFormGroupField(rootForm, control) {
return getGroupPath(rootForm, control);
}
/**
* This RxJS operator merges the value of the form with the raw value.
* By doing this we can assure that we don't lose values of disabled form fields
* @param form
*/
function mergeValuesAndRawValues(form) {
// Retrieve the standard values (respecting references)
const value = { ...form.value };
// Retrieve the raw values (including disabled values)
const rawValue = form.getRawValue();
// Recursive function to merge rawValue into value
function mergeRecursive(target, source) {
Object.keys(source).forEach((key) => {
if (target[key] === undefined) {
// If the key is not in the target, add it directly (for disabled fields)
target[key] = source[key];
}
else if (typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])) {
// If the value is an object, merge it recursively
mergeRecursive(target[key], source[key]);
}
// If the target already has the key with a primitive value, it's left as is to maintain references
});
}
mergeRecursive(value, rawValue);
return value;
}
function isPrimitive(value) {
return (value === null || (typeof value !== 'object' && typeof value !== 'function'));
}
/**
* Performs a deep-clone of an object
* @param obj
*/
function cloneDeep(obj) {
// Handle primitives (null, undefined, boolean, string, number, function)
if (isPrimitive(obj)) {
return obj;
}
// Handle Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Handle Array
if (Array.isArray(obj)) {
return obj.map((item) => cloneDeep(item));
}
// Handle Object
if (obj instanceof Object) {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = cloneDeep(obj[key]);
}
}
return clonedObj;
}
throw new Error("Unable to copy object! Its type isn't supported.");
}
/**
* Sets a value in an object in the correct path
* @param obj
* @param path
* @param value
*/
function set(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
/**
* Traverses the form and returns the errors by path
* @param form
*/
function getAllFormErrors(form) {
const errors = {};
if (!form) {
return errors;
}
function collect(control, path) {
if (control instanceof FormGroup || control instanceof FormArray) {
Object.keys(control.controls).forEach((key) => {
const childControl = control.get(key);
const controlPath = path ? `${path}.${key}` : key;
if (path && control.errors && control.enabled) {
Object.keys(control.errors).forEach((errorKey) => {
errors[path] = control.errors[errorKey];
});
}
if (childControl) {
collect(childControl, controlPath);
}
});
}
else {
if (control.errors && control.enabled) {
Object.keys(control.errors).forEach((errorKey) => {
errors[path] = control.errors[errorKey];
});
}
}
}
collect(form, '');
if (form.errors && form.errors['errors']) {
errors[ROOT_FORM] = form.errors && form.errors['errors'];
}
return errors;
}
class ValidateRootFormDirective {
constructor() {
this.validationOptions = input({ debounceTime: 0 });
this.destroy$$ = new Subject();
this.formValue = input(null);
this.suite = input(null);
/**
* Whether the root form should be validated or not
* This will use the field rootForm
*/
this.validateRootForm = input(false);
/**
* Used to debounce formValues to make sure vest isn't triggered all the time
*/
this.formValueCache = {};
}
validate(control) {
if (!this.suite() || !this.formValue()) {
return of(null);
}
return this.createAsyncValidator('rootForm', this.validationOptions())(control.getRawValue());
}
createAsyncValidator(field, validationOptions) {
if (!this.suite()) {
return () => of(null);
}
return (value) => {
if (!this.formValue()) {
return of(null);
}
const mod = cloneDeep(value);
set(mod, field, value); // Update the property with path
if (!this.formValueCache[field]) {
this.formValueCache[field] = {
sub$$: new ReplaySubject(1), // Keep track of the last model
};
this.formValueCache[field].debounced = this.formValueCache[field].sub$$.pipe(debounceTime(validationOptions.debounceTime));
}
// Next the latest model in the cache for a certain field
this.formValueCache[field].sub$$.next(mod);
return this.formValueCache[field].debounced.pipe(
// When debounced, take the latest value and perform the asynchronous vest validation
take(1), switchMap(() => {
return new Observable((observer) => {
this.suite()(mod, field).done((result) => {
const errors = result.getErrors()[field];
observer.next(errors ? { error: errors[0], errors } : null);
observer.complete();
});
});
}), takeUntil(this.destroy$$));
};
}
ngOnDestroy() {
this.destroy$$.next();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ValidateRootFormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: ValidateRootFormDirective, isStandalone: true, selector: "form[validateRootForm][formValue][suite]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null }, formValue: { classPropertyName: "formValue", publicName: "formValue", isSignal: true, isRequired: false, transformFunction: null }, suite: { classPropertyName: "suite", publicName: "suite", isSignal: true, isRequired: false, transformFunction: null }, validateRootForm: { classPropertyName: "validateRootForm", publicName: "validateRootForm", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: ValidateRootFormDirective,
multi: true,
},
], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ValidateRootFormDirective, decorators: [{
type: Directive,
args: [{
selector: 'form[validateRootForm][formValue][suite]',
standalone: true,
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: ValidateRootFormDirective,
multi: true,
},
],
}]
}] });
/**
* Clean error that improves the DX when making typo's in the `name` or `ngModelGroup` attributes
*/
class ShapeMismatchError extends Error {
constructor(errorList) {
super(`Shape mismatch:\n\n${errorList.join('\n')}\n\n`);
}
}
/**
* Validates a form value against a shape
* When there is something in the form value that is not in the shape, throw an error
* This is how we throw runtime errors in develop when the developer has made a typo in the `name` or `ngModelGroup`
* attributes.
* @param formVal
* @param shape
*/
function validateShape(formVal, shape) {
// Only execute in dev mode
if (isDevMode()) {
const errors = validateFormValue(formVal, shape);
if (errors.length) {
throw new ShapeMismatchError(errors);
}
}
}
/**
* Validates a form value against a shape value to see if it matches
* Returns clean errors that have a good DX
* @param formValue
* @param shape
* @param path
*/
function validateFormValue(formValue, shape, path = '') {
const errors = [];
for (const key in formValue) {
if (Object.keys(formValue).includes(key)) {
// In form arrays we don't know how many items there are
// This means that we always need to provide one record in the shape of our form array
// so every time reset the key to '0' when the key is a number and is bigger than 0
let keyToCompareWith = key;
if (parseFloat(key) > 0) {
keyToCompareWith = '0';
}
const newPath = path ? `${path}.${key}` : key;
if (typeof formValue[key] === 'object' && formValue[key] !== null) {
if ((typeof shape[keyToCompareWith] !== 'object' ||
shape[keyToCompareWith] === null) &&
isNaN(parseFloat(key))) {
errors.push(`[ngModelGroup] Mismatch: '${newPath}'`);
}
errors.push(...validateFormValue(formValue[key], shape[keyToCompareWith], newPath));
}
else if ((shape ? !(key in shape) : true) && isNaN(parseFloat(key))) {
errors.push(`[ngModel] Mismatch '${newPath}'`);
}
}
}
return errors;
}
class FormDirective {
constructor() {
this.ngForm = inject(NgForm, { self: true, optional: false });
/**
* The value of the form, this is needed for the validation part
*/
this.formValue = input(null);
/**
* Static vest suite that will be used to feed our angular validators
*/
this.suite = input(null);
/**
* The shape of our form model. This is a deep required version of the form model
* The goal is to add default values to the shape so when the template-driven form
* contains values that shouldn't be there (typo's) that the developer gets run-time
* errors in dev mode
*/
this.formShape = input(null);
/**
* Updates the validation config which is a dynamic object that will be used to
* trigger validations on the dependant fields
* Eg: ```typescript
* validationConfig = {
* 'passwords.password': ['passwords.confirmPassword']
* }
* ```
*
* This will trigger the updateValueAndValidity on passwords.confirmPassword every time the passwords.password gets a new value
*
* @param v
*/
this.validationConfig = input(null);
this.pending$ = this.ngForm.form.events.pipe(filter((v) => v instanceof StatusChangeEvent), map((v) => v.status), filter((v) => v === 'PENDING'), distinctUntilChanged());
/**
* Emits every time the form status changes in a state
* that is not PENDING
* We need this to assure that the form is in 'idle' state
*/
this.idle$ = this.ngForm.form.events.pipe(filter((v) => v instanceof StatusChangeEvent), map((v) => v.status), filter((v) => v !== 'PENDING'), distinctUntilChanged());
/**
* Triggered as soon as the form value changes
* Also every time Angular creates a new control or group
* It also contains the disabled values (raw values)
*/
this.formValueChange = this.ngForm.form.events.pipe(filter((v) => v instanceof ValueChangeEvent), map((v) => v.value), map(() => mergeValuesAndRawValues(this.ngForm.form)));
/**
* Emits an object with all the errors of the form
* every time a form control or form groups changes its status to valid or invalid
*/
this.errorsChange = this.ngForm.form.events.pipe(filter((v) => v instanceof StatusChangeEvent), map((v) => v.status), filter((v) => v !== 'PENDING'), map(() => getAllFormErrors(this.ngForm.form)));
/**
* Triggered as soon as the form becomes dirty
*/
this.dirtyChange = this.ngForm.form.events.pipe(filter((v) => v instanceof PristineChangeEvent), map((v) => !v.pristine), startWith(this.ngForm.form.dirty), distinctUntilChanged());
this.destroy$$ = new Subject();
/**
* Fired when the status of the root form changes.
*/
this.statusChanges$ = this.ngForm.form.statusChanges.pipe(startWith(this.ngForm.form.status), distinctUntilChanged());
/**
* Triggered When the form becomes valid but waits until the form is idle
*/
this.validChange = this.statusChanges$.pipe(filter((e) => e === 'VALID' || e === 'INVALID'), map((v) => v === 'VALID'), distinctUntilChanged());
/**
* Used to debounce formValues to make sure vest isn't triggered all the time
*/
this.formValueCache = {};
// When the validation config changes
// Listen to changes of the left-side of the config and trigger the updateValueAndValidity
// function on the dependant controls or groups at the right-side of the config
toObservable(this.validationConfig)
.pipe(filter((conf) => !!conf), switchMap((conf) => {
if (!conf) {
return of(null);
}
const streams = Object.keys(conf).map((key) => {
return this.ngForm?.form.get(key)?.valueChanges.pipe(
// Wait until something is pending
switchMap(() => this.pending$),
// Wait until the form is not pending anymore
switchMap(() => this.idle$), map(() => this.ngForm?.form.get(key)?.value), takeUntil(this.destroy$$), tap((v) => {
conf[key]?.forEach((path) => {
this.ngForm?.form.get(path)?.updateValueAndValidity({
onlySelf: true,
emitEvent: true,
});
});
}));
});
return zip(streams);
}))
.subscribe();
/**
* Trigger shape validations if the form gets updated
* This is how we can throw run-time errors
*/
this.formValueChange.pipe(takeUntil(this.destroy$$)).subscribe((v) => {
if (this.formShape()) {
validateShape(v, this.formShape());
}
});
/**
* Mark all the fields as touched when the form is submitted
*/
this.ngForm.ngSubmit.subscribe(() => {
this.ngForm.form.markAllAsTouched();
});
}
/**
* This will feed the formValueCache, debounce it till the next tick
* and create an asynchronous validator that runs a vest suite
* @param field
* @param validationOptions
* @returns an asynchronous validator function
*/
createAsyncValidator(field, validationOptions) {
if (!this.suite()) {
return () => of(null);
}
return (value) => {
if (!this.formValue()) {
return of(null);
}
const mod = cloneDeep(this.formValue());
set(mod, field, value); // Update the property with path
if (!this.formValueCache[field]) {
this.formValueCache[field] = {
sub$$: new ReplaySubject(1), // Keep track of the last model
};
this.formValueCache[field].debounced = this.formValueCache[field].sub$$.pipe(debounceTime(validationOptions.debounceTime));
}
// Next the latest model in the cache for a certain field
this.formValueCache[field].sub$$.next(mod);
return this.formValueCache[field].debounced.pipe(
// When debounced, take the latest value and perform the asynchronous vest validation
take(1), switchMap(() => {
return new Observable((observer) => {
this.suite()(mod, field).done((result) => {
const errors = result.getErrors()[field];
observer.next(errors ? { error: errors[0], errors } : null);
observer.complete();
});
});
}), takeUntil(this.destroy$$));
};
}
ngOnDestroy() {
this.destroy$$.next();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: FormDirective, isStandalone: true, selector: "form[scVestForm]", inputs: { formValue: { classPropertyName: "formValue", publicName: "formValue", isSignal: true, isRequired: false, transformFunction: null }, suite: { classPropertyName: "suite", publicName: "suite", isSignal: true, isRequired: false, transformFunction: null }, formShape: { classPropertyName: "formShape", publicName: "formShape", isSignal: true, isRequired: false, transformFunction: null }, validationConfig: { classPropertyName: "validationConfig", publicName: "validationConfig", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formValueChange: "formValueChange", errorsChange: "errorsChange", dirtyChange: "dirtyChange", validChange: "validChange" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormDirective, decorators: [{
type: Directive,
args: [{
selector: 'form[scVestForm]',
standalone: true,
}]
}], ctorParameters: () => [], propDecorators: { formValueChange: [{
type: Output
}], errorsChange: [{
type: Output
}], dirtyChange: [{
type: Output
}], validChange: [{
type: Output
}] } });
class ControlWrapperComponent {
constructor() {
this.ngModelGroup = inject(NgModelGroup, {
optional: true,
self: true,
});
this.destroy$$ = new Subject();
this.cdRef = inject(ChangeDetectorRef);
this.formDirective = inject(FormDirective);
}
get invalid() {
return this.control?.touched && this.errors;
}
get errors() {
if (this.control?.pending) {
return this.previousError;
}
else {
this.previousError = this.control?.errors?.['errors'];
}
return this.control?.errors?.['errors'];
}
get control() {
return this.ngModelGroup
? this.ngModelGroup.control
: this.ngModel?.control;
}
ngOnDestroy() {
this.destroy$$.next();
}
ngAfterViewInit() {
// Wait until the form is idle
// Then, listen to all events of the ngModelGroup or ngModel
// and mark the component and its ancestors as dirty
// This allows us to use the OnPush ChangeDetection Strategy
this.formDirective.idle$
.pipe(switchMap(() => this.ngModelGroup?.control?.events || of(null)), mergeWith(this.control?.events || of(null)), takeUntil(this.destroy$$))
.subscribe(() => {
this.cdRef.markForCheck();
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.1", type: ControlWrapperComponent, isStandalone: true, selector: "[sc-control-wrapper]", host: { properties: { "class.sc-control-wrapper--invalid": "this.invalid" } }, queries: [{ propertyName: "ngModel", first: true, predicate: NgModel, descendants: true }], ngImport: i0, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
type: Component,
args: [{ selector: '[sc-control-wrapper]', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n" }]
}], propDecorators: { ngModel: [{
type: ContentChild,
args: [NgModel]
}], invalid: [{
type: HostBinding,
args: ['class.sc-control-wrapper--invalid']
}] } });
/**
* Hooks into the ngModel selector and triggers an asynchronous validation for a form model
* It will use a vest suite behind the scenes
*/
class FormModelDirective {
constructor() {
this.validationOptions = input({ debounceTime: 0 });
this.formDirective = inject(FormDirective);
}
validate(control) {
const { ngForm, suite, formValue } = this.formDirective;
const field = getFormControlField(ngForm.control, control);
return this.formDirective.createAsyncValidator(field, this.validationOptions())(control.getRawValue());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: FormModelDirective, isStandalone: true, selector: "[ngModel]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: FormModelDirective,
multi: true,
},
], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngModel]',
standalone: true,
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: FormModelDirective,
multi: true,
},
],
}]
}] });
/**
* Hooks into the ngModelGroup selector and triggers an asynchronous validation for a form group
* It will use a vest suite behind the scenes
*/
class FormModelGroupDirective {
constructor() {
this.validationOptions = input({ debounceTime: 0 });
this.formDirective = inject(FormDirective);
}
validate(control) {
const { ngForm } = this.formDirective;
const field = getFormGroupField(ngForm.control, control);
return this.formDirective.createAsyncValidator(field, this.validationOptions())(control.value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.1", type: FormModelGroupDirective, isStandalone: true, selector: "[ngModelGroup]", inputs: { validationOptions: { classPropertyName: "validationOptions", publicName: "validationOptions", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: FormModelGroupDirective,
multi: true,
},
], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: FormModelGroupDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngModelGroup]',
standalone: true,
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: FormModelGroupDirective,
multi: true,
},
],
}]
}] });
/**
* This is borrowed from [https://github.com/wardbell/ngc-validate/blob/main/src/app/core/form-container-view-provider.ts](https://github.com/wardbell/ngc-validate/blob/main/src/app/core/form-container-view-provider.ts)
* Thank you so much Ward Bell for your effort!:
*
* Provide a ControlContainer to a form component from the
* nearest parent NgModelGroup (preferred) or NgForm.
*
* Required for Reactive Forms as well (unless you write CVA)
*
* @example
* ```
* @Component({
* ...
* viewProviders[ formViewProvider ]
* })
* ```
* @see Kara's AngularConnect 2017 talk: https://youtu.be/CD_t3m2WMM8?t=1826
*
* Without this provider
* - Controls are not registered with parent NgForm or NgModelGroup
* - Form-level flags say "untouched" and "valid"
* - No form-level validation roll-up
* - Controls still validate, update model, and update their statuses
* - If within NgForm, no compiler error because ControlContainer is optional for ngModel
*
* Note: if the SubForm Component that uses this Provider
* is not within a Form or NgModelGroup, the provider returns `null`
* resulting in an error, something like
* ```
* preview-fef3604083950c709c52b.js:1 ERROR Error:
* ngModelGroup cannot be used with a parent formGroup directive.
*```
*/
const formViewProvider = {
provide: ControlContainer,
useFactory: _formViewProviderFactory,
deps: [
[new Optional(), NgForm],
[new Optional(), NgModelGroup],
],
};
function _formViewProviderFactory(ngForm, ngModelGroup) {
return ngModelGroup || ngForm || null;
}
/**
* The providers we need in every child component that holds an ngModelGroup
*/
const vestFormsViewProviders = [
{ provide: ControlContainer, useExisting: NgForm },
formViewProvider, // very important if we want nested components with ngModelGroup
];
/**
* Exports all the stuff we need to use the template driven forms
*/
const vestForms = [
ValidateRootFormDirective,
ControlWrapperComponent,
FormDirective,
FormsModule,
FormModelDirective,
FormModelGroupDirective,
];
function arrayToObject(arr) {
return arr.reduce((acc, value, index) => ({ ...acc, [index]: value }), {});
}
/*
* Public API Surface of ngx-vest-forms
*/
/**
* Generated bundle index. Do not edit.
*/
export { ControlWrapperComponent, FormDirective, FormModelDirective, FormModelGroupDirective, ROOT_FORM, ShapeMismatchError, ValidateRootFormDirective, arrayToObject, cloneDeep, getAllFormErrors, getFormControlField, getFormGroupField, mergeValuesAndRawValues, set, validateShape, vestForms, vestFormsViewProviders };
//# sourceMappingURL=ngx-vest-forms.mjs.map