UNPKG

@studiohyperdrive/ngx-forms

Version:
1,177 lines (1,153 loc) 65.3 kB
import clean from 'obj-clean'; import { isValid, format, parse } from 'date-fns'; import * as i0 from '@angular/core'; import { forwardRef, inject, Injector, ChangeDetectorRef, Directive, ViewChildren, Input, Output, HostListener, InjectionToken, Optional, Inject } from '@angular/core'; import { tap as tap$1, takeUntil as takeUntil$1 } from 'rxjs/operators'; import * as i1 from '@angular/forms'; import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl } from '@angular/forms'; import { BehaviorSubject, Subject, filter, tap, takeUntil, combineLatest, startWith, of } from 'rxjs'; import { isEqual } from 'lodash'; const isEmptyInputValue = (value) => { // we don't check for string here so it also works with arrays return value == null || value.length === 0; }; /** * Removes an error from a form control * * @param control - Form control to remove the error from. * @param error - Name of the error to remove from the control. */ const clearFormError = (control, error) => { // Iben: Check if there are no errors existing on this control or if the the provided error does not exist, and early exit if needed const errors = new Set(Object.keys(control.errors || {})); if (errors.size === 0 || !errors.has(error)) { return; } // Iben: In case the provided error is the only error on the control, clear all errors and early exit if (errors.has(error) && errors.size === 1) { control.setErrors(null); return; } // Iben: In case there are more errors, remove only the provided error control.setErrors(clean({ ...control.errors, [error]: undefined, })); }; /** * Adds an error to a form control * * @param control - Form control to attach the error to. * @param error - Name of the error to attach to the control. * @param value - Value of the error being attached to the control */ const setFormError = (control, error, value = true) => { // Iben: Early exit in case the control already has the error if (control.hasError(error)) { return; } // Iben: Add the provided error control.setErrors({ ...control.errors, [error]: value, }); }; const EMPTY_SET$1 = new Set([undefined, null, '']); /** * FormGroup validator which checks if either all values or no values are filled in * * @param controls - An array of controls. * @param dependedControlKey - A control within the group which the other controls depend on. * @param matchFunction - Optional function the dependedControl should check */ const allOrNothingRequiredValidator = (form) => { const keys = Object.keys(form.value); // Iben: If the group is completely empty we clear all required errors if (Object.keys(clean(form.value, { preserveArrays: false })).length === 0) { for (const key of keys) { clearFormError(form.get(key), 'required'); } return null; } // Iben: Collect all control keys that are missing values const requiredKeys = new Set(); // Iben: Loop over all keys and check each control on whether it is empty or not keys.forEach((key) => { const control = form.get(key); // Iben: Check if the control is empty const isEmpty = typeof control.value === 'object' && control.value !== null ? Object.keys(clean(control.value)).length === 0 : EMPTY_SET$1.has(control.value); // Iben: Add/remove the errors when needed if (isEmpty) { setFormError(control, 'required'); requiredKeys.add(key); } else { clearFormError(control, 'required'); requiredKeys.delete(key); } }); // Iben: Return either null or the list of controls that are missing values based on the empty state return requiredKeys.size === 0 ? null : { allOrNothingRequiredError: Array.from(requiredKeys) }; }; /** * FormGroup validator which checks if either at least one value is filled in * * @param options - An optional object with configuration options, see below params for more info */ const atLeastOneRequiredValidator = (options) => { return (group) => { // Iben: Get the optional configuration items let conditionalFunction; let keys; if (options) { conditionalFunction = options.conditionalFunction; keys = options.controls; } // Iben: Setup the needed variables to handle the validator const cleanedFormValue = clean(group.value); const cleanedKeys = new Set(Object.keys(cleanedFormValue)); const controls = Object.values(group.controls); const empty = cleanedKeys.size === 0; // Iben: If nothing is filled in, we return an error if ((empty && !conditionalFunction) || (empty && conditionalFunction && conditionalFunction(group.value))) { for (const control of controls) { setFormError(control, 'required'); } return { atLeastOneRequiredError: true }; } // Iben: Check if we need to check on a specific key if (keys) { const hasOneKey = keys.reduce((hasOne, key) => hasOne || cleanedKeys.has(key), false); // Iben: Only return an error when there is no key matched at all // and in case of a conditionalFunction if the conditionalFunction is matched as well if ((!hasOneKey && !conditionalFunction) || (!hasOneKey && conditionalFunction && conditionalFunction(group.value))) { for (const key of keys) { setFormError(group.get(key), 'required'); } return { atLeastOneRequiredError: true }; } } // Iben: In case there are no errors, clean the required errors and return null for (const control of controls) { clearFormError(control, 'required'); } return null; }; }; const EMPTY_SET = new Set([undefined, null, '']); /** * FormGroup validator which checks if an array of controls in the control are filled in if the depended control is filled in * * @param controls - An array of controls. * @param dependedControlKey - A control within the group which the other controls depend on. * @param matchFunction - Optional function the dependedControl should check */ const dependedRequiredValidator = (controls, dependedControlKey, matchFunction) => { return (form) => { // Iben: Make a set so we know which controls are not filled in const keysWithErrors = new Set(); const dependedControl = form.get(dependedControlKey); // Iben: If the control is not filled in or the value doesn't match, we do an early exit and remove all potential required errors if (!dependedControl || !(matchFunction ? matchFunction(dependedControl.value) : !EMPTY_SET.has(dependedControl.value))) { for (const key of controls) { const control = form.get(key); // Continue if control does not exist if (!control) { continue; } clearFormError(control, 'required'); } return null; } // Iben: Set an overall error so we can see if all controls are filled in or not let hasError = false; for (const key of controls) { const control = form.get(key); // Iben: Continue if control does not exist if (!control) { continue; } hasError = hasError || EMPTY_SET.has(control.value); // Iben: If the control is not filled in we set a required error, if not, we remove it if (!EMPTY_SET.has(control.value)) { clearFormError(control, 'required'); keysWithErrors.delete(key); } else { setFormError(control, 'required'); keysWithErrors.add(key); } } const errors = Array.from(keysWithErrors); return hasError ? { hasDependedRequiredError: errors } : null; }; }; /** * Validates whether the inputted value has exceeded the maximum amount of decimals after the comma * * @param max - The maximum number of decimals after the comma */ const decimalsAfterCommaValidator = (max) => { return (control) => { // Iben: In case no control was provided, or the control value was empty, we early exit if (!control || (!control.value && control.value !== 0)) { return null; } // Iben: We check if the input value matches the amount of decimals after the comma, if not, we return an error return new RegExp(`^\\d+(.\\d{1,${max}})?$`).test(`${control.value}`) ? null : { invalidDecimalsAfterComma: true }; }; }; /** * A FormGroup validator to check whether a start and end date are chronologically correct * * @param startControlKey - The key of the control containing the start date value * @param endControlKey - The key of the control containing the end date value * @param format - Optional format of the dates provided by the controls, by default yyyy-MM-dd */ const chronologicalDatesValidator = (startControlKey, endControlKey, dateFormat = 'yyyy-MM-dd') => { return (form) => { // Iben: Get the date values const value = form.getRawValue(); const startValue = value[startControlKey]; const endValue = value[endControlKey]; // Iben: Clear the form error on the endControl clearFormError(form.get(endControlKey), 'incorrectChronologicalDate'); // Iben: If either date value is not filled in, we early exit to handle this in a potential required validator if (!startValue || !endValue) { return null; } // Iben: If the dates as is are not valid, early exit if (!isValid(new Date(startValue)) || !isValid(new Date(endValue))) { return null; } // Iben: Create dates so we can compare them const startDate = format(new Date(startValue), dateFormat); const endDate = format(new Date(endValue), dateFormat); // Iben: If either date is invalid based on the format, we early exit to handle this in a date validator if (!isValid(new Date(startDate)) || !isValid(new Date(endDate))) { return null; } // Iben: If the endDate falls before the startDate, we return an error if (endDate < startDate) { setFormError(form.get(endControlKey), 'incorrectChronologicalDate'); return { incorrectChronologicalDates: true }; } return null; }; }; const extendedEmailValidator = (control) => { if (isEmptyInputValue(control.value)) { return null; // don't validate empty values to allow optional controls } // Validates more strictly than the default email validator. Requires a period in the tld part. return /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]+$/gi.test(control.value) ? null : { extendedEmail: true }; }; /** * hasNoFutureDateValidator * * Validator function to ensure that the selected date is not in the future. * If the date is in the future, it returns an error. * @returns ValidationErrors if the date is in the future, otherwise null. * */ const hasNoFutureDateValidator = () => { return (control) => { // Early exit in case the control or the value does not exist if (!control.value) { return null; } // Create date objects based on the provided date and current date const inputDate = new Date(control.value); const currentDate = new Date(); // In case the date itself is invalid, we early exit to let a potential date validator handle the error if (!isValid(inputDate)) { return null; } return inputDate <= currentDate ? null : { isFutureDate: { valid: false } }; }; }; /** * Form control validator which validates if a date is between a provided range (edges not included) * * @param minDate - Minimum valid date * @param maxDate - Maximum valid date * @param format - Optional format used for all 3 dates, by default yyyy-MM-dd */ const dateRangeValidator = (min, max, format = 'yyyy-MM-dd') => { return (control) => { // Iben: Early exit in case the control or the value does not exist if (!control?.value) { return null; } // Iben : Create date objects based on the provided dates const date = parse(control.value, format, new Date()); const maxDate = parse(max, format, new Date()); const minDate = parse(min, format, new Date()); // Iben: In case either of the boundary dates is invalid, we mark the input as invalid as we cannot confirm it's in the right range if (!isValid(maxDate) || !isValid(minDate)) { return { invalidRange: !isValid(maxDate) ? 'invalidMaxDate' : 'invalidMinDate', }; } // Iben: In case the date itself is invalid, we early exit to let a potential date validator handle the error if (!isValid(date)) { return null; } // Iben: We check if the date is in between the boundaries and return an error if need be if (!(minDate <= date) || !(date <= maxDate)) { return { invalidRange: date > maxDate ? 'dateAfterMaxDate' : 'dateBeforeMinDate', }; } return null; }; }; /** * WordCountValidator * * The WordCountValidator validator will check the amount of words provided in a control. * * @param .min * @param .max * @returns ValidatorFn */ const WordCountValidator = ({ min, max }) => { return (control) => { if (typeof control?.value !== 'string' || (typeof min !== 'number' && typeof max !== 'number')) { return null; } const wordCount = control.value.trim().split(' ').length; if (typeof min === 'number' && wordCount <= min) { return { minWordCountNotReached: { valid: false } }; } if (typeof max === 'number' && wordCount > max) { return { maxWordCountReached: { valid: false } }; } return null; }; }; /** * CompareValidator * * The CompareValidator will return a validator that compares the values of two FormControls * within a FormGroup based on a given comparator function. * * Note: This validator will only set an error on the group it is set to * unless the `setErrorOnKey` argument is given. * * @param keys {string[]} * @param comparatorFn {(...args: ValueType[]) => boolean} * @param setErrorOnKey {string} * @returns {(group: FormGroup<{ [key: string]: FormControl<ValueType>; }>) => ValidationErrors} */ const CompareValidator = (keys, comparatorFn, setErrorOnKey) => { return (group) => { // Denis: map the values to an array: const values = keys.map((key) => group?.get(key).getRawValue()); const setErrorOnKeyControl = group?.get(setErrorOnKey); // Denis: check if any of the keys contains an undefined or null value: if (values.some((value) => typeof value === 'undefined' || value === null)) { setErrorOnKeyControl && clearFormError(group.get(setErrorOnKey), 'compareError'); return null; } if (comparatorFn(...values)) { setErrorOnKeyControl && setFormError(group.get(setErrorOnKey), 'compareError'); return { compareError: true, }; } setErrorOnKeyControl && clearFormError(group.get(setErrorOnKey), 'compareError'); return null; }; }; /** * Exported Class */ class NgxValidators { /** * A stricter validator for e-mail validation * * @param control - A form control */ static extendedEmail(control) { return extendedEmailValidator(control); } /** * A validator to check if all or none of the values of a form group are filled in. * Particularly useful in situations where a form group field within itself is optional, * but all fields are required in case it does get filled in * * Returns an `allOrNothingRequiredError` error on the provided FormGroup and a `required` error on the individual controls * * @param control - A form group control */ static allOrNothingRequired(control) { return allOrNothingRequiredValidator(control); } /** * A validator to check if at least one of the provided controls of the form group are filled in * * Returns an `atLeastOneRequiredError` error on the provided FormGroup and a `required` error on the individual controls * * @param options - An optional object with configuration options, see below params for more info * @param controlNames - Optional list of controls, if not provided the validator is applied to all controls of the group * @param conditionalFunction - Optional function the form value needs to return true to for the required to be se */ static atLeastOneRequired(options) { return atLeastOneRequiredValidator(options); } /** * The compareValidator will return a validator that compares the values of two FormControls * within a FormGroup based on a given comparator function. * * Returns a `compareError` on the provided FormGroup and on the individual controls if the `setErrorKey` argument is provided. * * @param keys {string[]} * @param comparatorFn {(...args: ValueType[]) => boolean} * @param setErrorOnKey {string} * @returns {(group: FormGroup<{ [key: string]: FormControl<ValueType>; }>) => ValidationErrors} * */ static compareValidator(keys, comparatorFn, setErrorOnKey) { return CompareValidator(keys, comparatorFn, setErrorOnKey); } /** * FormGroup validator which checks if an array of controls in the control are filled in if the depended control is filled in * * Returns a `hasDependedRequiredError` error on the provided FormGroup and a `required` error on the individual controls * * @param controls - An array of controls. * @param dependedControlKey - A control within the group which the other controls depend on. * @param matchFunction - Optional function the dependedControl should check */ static dependedRequired(controls, dependedControlKey, matchFunction) { return dependedRequiredValidator(controls, dependedControlKey, matchFunction); } /** * Validates whether the inputted value has exceeded the maximum amount of decimals after the comma * * Returns an `invalidDecimalsAfterComma` error on the provided control * * @param max - The maximum number of decimals after the comma */ static decimalsAfterComma(max) { return decimalsAfterCommaValidator(max); } /** * A FormGroup validator to check whether a start and end date are chronologically correct * * Returns an `incorrectChronologicalDates` error on the provided FormGroup and a `incorrectChronologicalDate` on the endControl * * @param startControlKey - The key of the control containing the start date value * @param endControlKey - The key of the control containing the end date value * @param format - Optional format of the dates provided by the controls, by default yyyy-MM-dd */ static chronologicalDates(startControlKey, endControlKey, format = 'yyyy-MM-dd') { return chronologicalDatesValidator(startControlKey, endControlKey, format); } /** * Form control validator which validates if a date is between a provided range * * Returns an `invalidRange` error * * @param minDate - Minimum valid date * @param maxDate - Maximum valid date * @param format - Optional format used for all 3 dates, by default yyyy-MM-dd */ static dateRangeValidator(min, max, format = 'yyyy-MM-dd') { return dateRangeValidator(min, max, format); } /** * Form control validator which validates if a date is not in the future. * * Returns an `isFutureDate` error */ static { this.hasNoFutureDateValidator = () => { return hasNoFutureDateValidator(); }; } /** * Form control validator which validates if a provided string does not contain more or less words than a provided min and/or max. * * Returns either a `minWordCountNotReached` or a `maxWordCountReached` */ static { this.wordCountValidator = ({ min, max }) => { return WordCountValidator({ min, max }); }; } } /** * In order to select all accessors in a FormContainer, we need this base class to pass to our ViewChildren. * * IMPORTANT: This will never be used as an actual functional component */ class BaseFormAccessor { } /** * Allows for a deep markAsDirty of all controls. Can be used for a FormGroup or a FormArray * * @param controls - The controls we wish to update the value and validity of * @param onlySelf - Whether or not we want it to be only the control itself and not the direct ancestors. Default this is true */ const markAllAsDirty = (controls, options = {}) => { // Iben: We loop over all controls (Array.isArray(controls) ? controls : Object.values(controls)).forEach((control) => { // Iben: If there are no child controls, we update the value and validity of the control if (!control['controls']) { control.markAsDirty(options); return; } // Iben: If there are child controls, we recursively update the value and validity markAllAsDirty(control['controls'], options); }); }; /** * Adds a deep update value and validity to the existing update value and validity * * @param form - The provided abstract control * @param options - The options we wish to call along with the update value and validity function */ const updateAllValueAndValidity = (form, options = {}) => { // Iben: Call the original updateValueAndValidity form.updateValueAndValidity(options); // Iben: If we don't have the inner form yet we just do the default update value if (!form || !form['controls']) { return; } // Iben: We update the value and validity recursively for each child control deepUpdateValueAndValidity(form['controls'], { ...options, onlySelf: true }); }; /** * Allows for a deep updateValueAndValidity of all controls. Can be used for a FormGroup or a FormArray * * @param controls - The controls we wish to update the value and validity of * @param onlySelf - Whether or not we want it to be only the control itself and not the direct ancestors. Default this is true */ const deepUpdateValueAndValidity = (controls, options = {}) => { // Iben: We loop over all controls (Array.isArray(controls) ? controls : Object.values(controls)).forEach((control) => { // Iben: If there are no child controls, we update the value and validity of the control if (!control['controls']) { control.updateValueAndValidity(options); return; } // Iben: If there are child controls, we recursively update the value and validity deepUpdateValueAndValidity(control['controls'], options); }); }; /** * Disable a FormControl/FormArray * * @param keys - The keys of the fields we wish to disable * @param emitEvent - Whether or not we wish to emit the event */ const handleDisableFormControlOfFormArray = (form, keys, emitEvent) => { // Iben: Early exit in case the state already matches so we don't do unnecessary emits if ((keys.has('formAccessorSelf') && form.disabled) || (!keys.has('formAccessorSelf') && form.enabled)) { return; } // Iben: Disable/enable the control based on the key keys.has('formAccessorSelf') ? form.disable({ emitEvent }) : form.enable({ emitEvent }); }; /** * Disable the controls of a FormGroup * * @param keys - The keys of the fields we wish to disable * @param emitEvent - Whether or not we wish to emit the event */ const handleDisableFormGroup = (form, keys, emitEvent) => { // Iben: Loop over all controls and enable them so that they are re-enabled in case the set of keys changes enableControls(form, emitEvent); // Iben: Disable the keys Array.from(keys).forEach((key) => { const control = form.get(key); if (!control) { console.warn(`FormAccessor: The key "${key}" was provided in the disableFields array but was not found in the provided form.`); return; } // Iben: Prevent emit event if the control is already disabled if (!control.disabled) { control.disable({ emitEvent }); } }); }; /** * Recursively enables all fields of a control * * @param control - An AbstractControl which we want to enable and enable all children off * @param emitEvent - Whether or not we wish to emit an event */ const enableControls = (control, emitEvent = false) => { //Iben: If no control was found, early exit if (!control) { return; } // Iben: Enable the control itself if it is not enabled yet if (!control.enabled) { control.enable({ emitEvent }); } // Iben: If there are no controls, early exit if (!control['controls']) { return; } // Iben: Recursively enable each control (Array.isArray(control['controls']) ? control['controls'] : Object.values(control['controls'])).forEach((child) => { enableControls(child, emitEvent); }); }; /** * Disables and enables a form's control based on a set of provided keys * * @param form - The form we wish to disable the controls for * @param controlKeys - A set of keys of the controls * @param emitEvent - Whether or not we wish to emit the event */ const handleFormAccessorControlDisabling = (form, controlKeys, emitEvent) => { // Iben: Depending on whether we're dealing with a FormArray/FormControl or a FormGroup, we have different and handle the disable/enable state if (!form['controls'] || Array.isArray(form['controls'])) { handleDisableFormControlOfFormArray(form, controlKeys, emitEvent); } else { handleDisableFormGroup(form, controlKeys, emitEvent); } }; /** * Marks a form and all the form-accessors this form is based on as dirty * * @param form - The form we wish to mark as dirty * @param accessors - An array of all the accessors we wish to mark as dirty * @param options - Form state options we wish to provide */ const handleFormAccessorMarkAsDirty = (form, accessors, options = {}) => { // Iben: If the control has child controls, recursively mark them as dirty if (form['controls']) { markAllAsDirty(form['controls'], options); } else { // Iben : Mark the form as dirty form.markAsDirty(options); } // Iben: Loop over each form accessor and call the mark as dirty function, so all subsequent accessors are also marked as dirty accessors.forEach((accessor) => accessor.markAsDirty(options)); }; /** * Marks a form and all the form-accessors this form is based on as touched * * @param form - The form we wish to mark as touched * @param accessors - An array of all the accessors we wish to mark as touched * @param options - Form state options we wish to provide */ const handleFormAccessorMarkAsTouched = (form, accessors, options = {}) => { // Iben: Mark all the controls and the children as touched form.markAllAsTouched(); // Iben: Loop over each form accessor and call the mark as touched function, so all subsequent accessors are also marked as touched accessors.forEach((accessor) => accessor.markAsTouched(options)); }; /** * Marks a form and all the form-accessors this form is based on as pristine * * @param form - The form we wish to mark as pristine * @param accessors - An array of all the accessors we wish to mark as pristine * @param options - Form state options we wish to provide */ const handleFormAccessorMarkAsPristine = (form, accessors, options = {}) => { // Iben: Mark all the controls and the children as touched form.markAsPristine(); // Iben: Loop over each form accessor and call the mark as touched function, so all subsequent accessors are also marked as touched accessors.forEach((accessor) => accessor.markAsPristine(options)); }; /** * Updates a form and all the form-accessors this form i * * @param form - The form we wish to update the value and validity of * @param accessors - An array of all the accessors we wish to update the value and validity of * @param options - Form state options we wish to provide */ const handleFormAccessorUpdateValueAndValidity = (form, accessors, options = {}) => { // Iben: Update the value and validity of the form updateAllValueAndValidity(form, options); // Iben: Loop over each form accessor and call the updateValueAndValidity function, so all subsequent accessors are also updated accessors.forEach((accessor) => accessor.updateAllValueAndValidity(options)); }; /** * Recursively checks if a form and its possible children have an error * * @param control - The provided abstract control */ const hasErrors = (control) => { // Iben: If the form has no children we just return the state of the current form if (!control['controls']) { return control.invalid; } // Iben: If the form has children, we check if some of the child controls have errors const controls = control['controls']; return (Array.isArray(controls) ? controls : Object.values(controls)).some((control) => hasErrors(control)); }; /** * Listen to the touched event of a control * * @param control - An AbstractControl */ const touchedEventListener = (control) => { // Iben: Grab the current markAsTouched and UnTouched methods const markAsTouched = control.markAsTouched; const markAsUnTouched = control.markAsUntouched; // Iben: Set a subject with the current touched state const touchedSubject = new BehaviorSubject(control.touched); // Iben: Overwrite the existing functions and emit the touched state control.markAsTouched = (options) => { touchedSubject.next(true); markAsTouched.bind(control)(options); }; control.markAsUntouched = (options) => { touchedSubject.next(false); markAsUnTouched.bind(control)(options); }; // Iben: Return the touched state return touchedSubject.asObservable(); }; /** * Generates the necessary providers for a (Data)FormAccessor. * * @param component - The component class of the (Data)FormAccessor */ const createAccessorProviders = (component) => { return [ // Iben: Generate a provider for the control handling { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => component), multi: true, }, // Iben: Generate a provider for the validation handling { provide: NG_VALIDATORS, useExisting: forwardRef(() => component), multi: true, }, // Iben: Generate a provider for the FormContainer handling { provide: BaseFormAccessor, useExisting: forwardRef(() => component), }, ]; }; class NgxFormsControlValueAccessor { /** * Keys of the fields we wish to disable. * By default this will emit a valueChanges, this can be overwritten by the emitValueWhenDisableFieldsUsingInput in the Accessor * * @memberof FormAccessor */ set disableFields(keys) { // Iben: Early exit in case the keys are not provided if (!keys) { return; } // Iben: Setup a subject to track whether we're still disabling the fields const disabling = new Subject(); // Iben: Add the keys to a set for more performant lookup and convert those to a string to not have Typescript issues later down the line const controlKeys = new Set(keys); // Iben: Check if we need to dispatch the disable or enable event const emitEvent = this.emitValueWhenDisableFieldsUsingInput ? this.emitValueWhenDisableFieldsUsingInput(keys) : true; // Iben: Listen to the initialized state of the form this.initialized$ .pipe(filter(Boolean), tap(() => { // TODO: Iben: Remove this setTimeout once we're in a Signal based component setTimeout(() => { // Iben: Handle the disabling of the fields handleFormAccessorControlDisabling(this.form, controlKeys, emitEvent); }); // Iben: Set the disabling subject so that we can complete this subscription disabling.next(undefined); disabling.complete(); }), takeUntil(disabling)) .subscribe(); } constructor() { /** * The Injector needed in the constructor */ this.injector = inject(Injector); /** * The ChangeDetector reference */ this.cdRef = inject(ChangeDetectorRef); /** * A subject to hold the parent control */ this.parentControlSubject$ = new Subject(); /** * A reference to the control tied to this control value accessor */ this.parentControl$ = this.parentControlSubject$.pipe(filter(Boolean)); /** * Whether the first setDisable has run */ this.initialSetDisableHasRun = false; /** * On destroy flow handler */ this.destroy$ = new Subject(); /** * Subject to check whether the form is initialized */ this.initializedSubject$ = new BehaviorSubject(false); /** * Whether we want to skip the first setDisable (https://github.com/angular/angular/pull/47576). * By default, this is true */ this.skipInitialSetDisable = true; /** * Stream to know whether the form has been initialized */ this.initialized$ = this.initializedSubject$.asObservable(); /** * Sets up the ControlValueAccessor connectors */ this.onTouch = () => { }; // tslint:disable-line:no-empty this.onChange = (_) => { }; // tslint:disable-line:no-empty // Iben: Use setTimeOut to avoid the circular dependency issue setTimeout(() => { try { const parentControl = this.injector.get(NgControl); // Iben: If for some reason we can't find the control or the ngControl, early exit and throw an error if (!parentControl?.control) { console.error('NgxForms: No control was found after initializing. Check if a control was assigned to the FormAccessor.'); return; } this.parentControlSubject$.next(parentControl.control); // Iben: Grab the control from the parent container const control = parentControl.control; // Iben: Setup the markAsTouched flow // Iben: Keep a reference to the original `markAsTouched` handler. const markAsTouched = control.markAsTouched.bind(control); // Iben: Override the `markAsTouched` handler with our own. control.markAsTouched = (options) => { // Iben: If the control is already marked as touched, we early exit if (control.touched) { return; } // Iben: Invoke the original `markAsTouchedHandler`. markAsTouched(options); // Iben: If the onlySelf flag is set to true, we early exit if (options?.onlySelf) { return; } // Iben: Invoke the custom `markAsTouchedHandler`. this.markAsTouched(options); }; // Iben: Setup the markAsDirty flow // Iben: Keep a reference to the original `markAsDirty` handler. const markAsDirty = control.markAsDirty.bind(control); // Iben: Override the `markAsDirty` handler with our own. control.markAsDirty = (options) => { // Iben: If the control is already marked as dirty, we early exit if (control.dirty) { return; } // Iben: Invoke the original `markAsDirtyHandler`. markAsDirty(options); // Iben: If the onlySelf flag is set to true, we early exit if (options?.onlySelf) { return; } // Iben: Invoke the custom `markAsDirtyHandler`. this.markAsDirty(options); }; // Iben: Setup the markAsPristine flow // Iben: Keep a reference to the original `markAsPristine` handler. const markAsPristine = control.markAsPristine.bind(control); // Iben: Override the `markAsPristine` handler with our own. control.markAsPristine = (options) => { // Iben: If the control is already marked as pristine, we early exit if (control.pristine) { return; } // Iben: Invoke the original `markAsPristineHandler`. markAsPristine(options); // Iben: If the onlySelf flag is set to true, we early exit if (options?.onlySelf) { return; } // Iben: Invoke the custom `markAsPristineHandler`. this.markAsPristine(options); }; } catch (error) { console.warn('NgxForms: No parent control was found while trying to set up the form accessor.'); } }); } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouch = fn; } /** * Writes value to the inner form * * @param value - Value to patch in the inner form */ writeValue(value) { // Iben: Early exit in case the form was not found if (!this.form) { console.error('NgxForms: No form was found when trying to write a value. This error can occur when overwriting the ngOnInit without invoking super.OnInit().'); return; } // Iben: Reset the current form without emitEvent to not trigger the valueChanges this.form.reset(undefined, { emitEvent: false }); // Iben: Patch the current form with the new value without emitEvent to not trigger the valueChanges if (value !== undefined && value !== null) { this.form.patchValue(this.onWriteValueMapper ? this.onWriteValueMapper(value) : value, { emitEvent: false, }); } // Iben: Validate the current value this.validate(); // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } /** * Mark all controls of the form as touched */ markAsTouched(options = {}) { handleFormAccessorMarkAsTouched(this.form, this.accessors?.toArray() || [], options); // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } /** * Mark all controls of the form as dirty */ markAsDirty(options = {}) { handleFormAccessorMarkAsDirty(this.form, this.accessors?.toArray() || [], options); // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } /** * Mark all controls of the form as pristine */ markAsPristine(options = {}) { handleFormAccessorMarkAsPristine(this.form, this.accessors?.toArray() || [], options); // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } /** * Update the value and validity of the provided form */ updateAllValueAndValidity(options) { handleFormAccessorUpdateValueAndValidity(this.form, this.accessors?.toArray() || [], options); // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } /** * Validates the inner form */ validate() { // Iben: If the form itself is invalid, we return the invalidForm: true right away if (this.form.invalid) { return { invalidForm: true }; } // Iben: In case the form is invalid, we check if the child controls are possibly invalid return hasErrors(this.form) ? { invalidForm: true } : null; } /** * Disables/enables the inner form based on the passed value * * @param isDisabled - Whether or not the form should be disabled */ setDisabledState(isDisabled) { // Iben: Skip the initial setDisabled, as this messes up our form approach. // https://github.com/angular/angular/pull/47576 if (this.skipInitialSetDisable && !this.initialSetDisableHasRun) { this.initialSetDisableHasRun = true; return; } if (isDisabled) { this.form.disable({ emitEvent: false }); } else { this.form.enable({ emitEvent: false }); } // Iben: Detect changes so the changes are visible in the dom this.cdRef.detectChanges(); } ngOnDestroy() { this.destroy$.next(undefined); this.destroy$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxFormsControlValueAccessor, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.3", type: NgxFormsControlValueAccessor, isStandalone: true, inputs: { disableFields: "disableFields", skipInitialSetDisable: "skipInitialSetDisable" }, outputs: { initialized$: "initialized$" }, viewQueries: [{ propertyName: "accessors", predicate: BaseFormAccessor, descendants: true }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: NgxFormsControlValueAccessor, decorators: [{ type: Directive }], ctorParameters: () => [], propDecorators: { accessors: [{ type: ViewChildren, args: [BaseFormAccessor] }], disableFields: [{ type: Input }], skipInitialSetDisable: [{ type: Input }], initialized$: [{ type: Output }] } }); class FormAccessor extends NgxFormsControlValueAccessor { ngOnInit() { // Iben: Set the inner form this.form = this.initForm(); // Iben: Early exit in case the form was not found if (!this.form) { console.error('NgxForms: No form was found after initializing. Check if the initForm method returns a form.'); return; } // Iben: Warn the initialized$ observable that the form has been set up this.initializedSubject$.next(true); // Iben: Listen to the changes and warn the parent form this.form.valueChanges .pipe(tap$1((value) => { // In case there's a mapper we map the value, else we send the form value this.onChange(this.onChangeMapper ? this.onChangeMapper(value) : value); }), takeUntil$1(this.destroy$)) .subscribe(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FormAccessor, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.3", type: FormAccessor, isStandalone: true, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FormAccessor, decorators: [{ type: Directive }] }); class DataFormAccessor extends NgxFormsControlValueAccessor { set data(data) { // Iben: If we already have current data and the current data matches the new data, we don't make a new form if (this.currentData && isEqual(this.currentData, data)) { this.currentData = data; return; } this.initializedSubject$.next(false); this.currentData = data; // Iben: Emit to the destroy so the previous subscription is cancelled this.destroy$.next(undefined); // Set the inner form this.form = this.initForm(data); // Iben: Early exit in case the form was not found if (!this.form) { console.error('NgxForms: No form was found after initializing. Check if the initForm method returns a form.'); return; } // Denis: set the initialized property this.setInitializedWithData(data); // Iben: Check if the form is valid depending on the provided value this.validate(); this.cdRef.detectChanges(); // Iben: Subscribe to the value changes this.form.valueChanges .pipe(tap$1((value) => { // In case there's a mapper we map the value, else we send the form value this.onChange(this.onChangeMapper ? this.onChangeMapper(value) : value); }), takeUntil$1(this.destroy$)) .subscribe(); } /** * setInitialized * * This method sets the initialized property to true when the form is initialized. * This functionality has been moved to a separate method to enable * overwriting this method to fit certain use-cases. * * @param {ConstructionDateType} data * @returns void * @private */ setInitializedWithData(data) { this.initializedSubject$.next(Array.isArray(data) ? data && data.length > 0 : !!data); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: DataFormAccessor, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.3", type: DataFormAccessor, isStandalone: true, inputs: { data: "data" }, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: DataFormAccessor, decorators: [{ type: Directive }], propDecorators: { data: [{ type: Input, args: [{ required: true }] }] } }); class FormAccessorContainer { constructor() { /** * Destroyed state of the component */ this.destroyed$ = new Subject(); } /** * @deprecated This method should no longer be used, use the markAsDirty on the form itself instead * * Marks the form and all the inputs of every subsequent form-accessors as dirty * * @param form - The form used in the component * @param options - Options passed to the form state changer */ markAllAsDirty(form, options = {}) { this.handleAccessorsAction(() => { handleFormAccessorMarkAsDirty(form, this.accessors?.toArray() || [], options); }); } /** * @deprecated This method should no longer be used, use the markAsTouched on the form itself instead * * Marks the form and all the inputs of every subsequent form-accessors as touched * * @param form - The form used in the component * @param options - Options passed to the form state changer */ markAllAsTouched(form, options = {}) { this.handleAccessorsAction(() => { handleFormAccessorMarkAsTouched(form, this.accessors?.toArray() || [], options); }); } /** * Updates the value and validity of the form and all the inputs of every subsequent form-accessors * * @param form - The provided forms * @param options - Options passed to the updateValueAndValidity */ updateAllValueAndValidity(form, options = {}) { this.handleAccessorsAction(() => { handleFormAccessorUpdateValueAndValidity(form, this.accessors?.toArray() || [], options); }); } /** * Handle the destroy state of the component */ ngOnDestroy() { this.destroyed$.next(undefined); this.destroyed$.complete(); } /** * Handle the accessors action of the FormContainer and throw a warning if no accessors are provided * * @param action - The provided action */ handleAccessorsAction(action) { // Iben: Throw a warn in case there are no accessors found if (!this.accessors || this.accessors?.toArray().length === 0) { console.warn('NgxForms: No (Data)FormAccessors were found in this component. Check if each (Data)FormAccessor also provides the BaseFormAccessor in its providers array. If this is intentional, this warning can be ignored.'); } // Iben: Handle the provided action action(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FormAccessorContaine