UNPKG

jlf-typed-form-craft

Version:
332 lines (313 loc) 13.1 kB
import * as i0 from '@angular/core'; import { Injectable, Component } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import 'reflect-metadata'; import { Subscription, zip } from 'rxjs'; class TypedFormCraftService { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypedFormCraftService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypedFormCraftService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypedFormCraftService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class TypedFormCraftComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypedFormCraftComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TypedFormCraftComponent, isStandalone: true, selector: "lib-typed-form-craft", ngImport: i0, template: ` <p> typed-form-craft works! </p> `, isInline: true, styles: [""] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TypedFormCraftComponent, decorators: [{ type: Component, args: [{ selector: 'lib-typed-form-craft', imports: [], template: ` <p> typed-form-craft works! </p> ` }] }] }); const FORM_CONTROL_METADATA = 'form:control:metadata'; const FORM_GROUP_METADATA = 'form:group:metadata'; // Updated Form Factory /** * Creates a `DestroyableFormGroup` instance from a given class instance, using metadata to configure form controls and * validators. It maps the class instance's properties to form controls, applying default values, synchronous validators, * asynchronous validators, and enable/disable conditions where applicable. * * @template T The type of the class instance used for creating the form. * @param {T} instance An object instance of the class for which form controls should be created. * @returns {DestroyableFormGroup<T>} A reactive form group enhanced with a `destroy` method for cleanup. * * The method extracts metadata from the class instance to dynamically configure the form: * - Metadata specifies form control values, validators, and conditions for enabling or disabling controls. * - Validators and async validators are applied after the form group is created. * - The enable/disable logic listens to form changes and adjusts controls dynamically. * * The returned `DestroyableFormGroup` includes a `destroy` method, which unsubscribes from any internal subscriptions, * ensuring proper resource cleanup. */ /*export const createFormFromClass = <T extends Record<string, any>>(instance: T): DestroyableFormGroup<T> => { const sub = new Subscription(); const metadata: Record<string, FormControlOptions> | undefined = Reflect.getMetadata( FORM_CONTROL_METADATA, instance.constructor.prototype ); const controls: { [key: string]: FormControl } = {}; // First: create all the controls if (metadata) { for (const propertyKey in metadata) { if (Object.prototype.hasOwnProperty.call(metadata, propertyKey) && Object.prototype.hasOwnProperty.call(instance, propertyKey)) { const options: FormControlOptions = metadata[propertyKey]; const control = new FormControl( options.defaultValue !== undefined ? options.defaultValue : instance[propertyKey] ); controls[propertyKey] = control; } } } // Create the FormGroup with controls const formGroup = new FormGroup(controls); // Second: configure validators after the FormGroup is created if (metadata) { for (const propertyKey in metadata) { const options = metadata[propertyKey]; const control = controls[propertyKey]; if (control) { if (options.validators) { control.setValidators(options.validators); } if (options.asyncValidators) { control.setAsyncValidators(options.asyncValidators); } control.updateValueAndValidity(); } } } // Configure enable/disable conditions if (metadata) { for (const propertyKey in metadata) { const options = metadata[propertyKey]; const control = controls[propertyKey]; if (options.disable) { const shouldDisable = options.disable(formGroup); if (shouldDisable) { control.disable({ emitEvent: false }); } sub.add( zip(formGroup.valueChanges, formGroup.statusChanges).subscribe({ next: () => { const shouldDisable = options.disable(formGroup); if (shouldDisable && control.enabled) { control.disable(); } else if (!shouldDisable && control.disabled) { control.enable(); } } }) ) } } } return Object.assign(formGroup, { destroy: () => sub.unsubscribe() }) as DestroyableFormGroup<T>; }*/ const createFormFromClass = (instance) => { const sub = new Subscription(); const controlMetadata = Reflect.getMetadata(FORM_CONTROL_METADATA, instance.constructor.prototype); const controls = {}; if (controlMetadata) { for (const propertyKey in instance) { if (instance.hasOwnProperty(propertyKey)) { const options = controlMetadata[propertyKey]; if (options) { // Create a FormControl for properties with @controlProp const control = new FormControl(options.defaultValue !== undefined ? options.defaultValue : instance[propertyKey]); controls[propertyKey] = control; } else { // Assume it's a nested object and create a FormGroup for properties with @controlGroupProp if (typeof instance[propertyKey] === 'object' && instance[propertyKey] !== null) { const nestedFormGroup = createFormFromClass(instance[propertyKey]); controls[propertyKey] = nestedFormGroup; } } } } } const formGroup = new FormGroup(controls); // Configure validators and async validators if (controlMetadata) { for (const propertyKey in controlMetadata) { const options = controlMetadata[propertyKey]; const control = controls[propertyKey]; if (control) { if (options.validators) { control.setValidators(options.validators); } if (options.asyncValidators) { control.setAsyncValidators(options.asyncValidators); } control.updateValueAndValidity(); } } } // Configure enable/disable conditions if (controlMetadata) { for (const propertyKey in controlMetadata) { const options = controlMetadata[propertyKey]; const control = controls[propertyKey]; if (options.disable) { const shouldDisable = options.disable(formGroup); if (shouldDisable) { control.disable({ emitEvent: false }); } sub.add(zip(formGroup.valueChanges, formGroup.statusChanges).subscribe({ next: () => { const shouldDisable = options.disable(formGroup); if (shouldDisable && control.enabled) { control.disable(); } else if (!shouldDisable && control.disabled) { control.enable(); } } })); } } } return Object.assign(formGroup, { destroy: () => sub.unsubscribe() }); }; /** * A custom validator function that applies conditional validation logic * based on a user-defined configuration. * * The function evaluates a `condition` provided in the configuration to determine * whether the associated `validator` function should be applied to the control. * It also tracks changes to relevant fields in the form and revalidates the control * whenever any of the dependent fields are updated. * * This supports dynamic validation scenarios where the validity of a control * is determined by its relationship with other controls in the form. * * @param {ConditionalValidatorConfig} config - The configuration object containing * the condition function and the validator. * @returns {ValidatorFn} A validator function that implements the conditional validation logic. */ const conditionalValidator = (config) => { let lastForm = null; let subscriptions = new Subscription(); return (control) => { const form = control.parent; if (form && form !== lastForm) { subscriptions.unsubscribe(); subscriptions = new Subscription(); // execute the condition a first time by intercepting the calls to get() const watchedFields = new Set(); const originalGet = form.get.bind(form); form.get = (path) => { watchedFields.add(path); return originalGet(path); }; // Test execution to detect used fields config.condition(control); // Restore the original get method form.get = originalGet; // Setting up monitoring for detected fields watchedFields.forEach(fieldName => { const field = form.get(fieldName); if (field) { subscriptions.add(zip(field.valueChanges, field.statusChanges).subscribe({ next: () => (control.updateValueAndValidity({ emitEvent: false })) })); } }); lastForm = form; } return config.condition(control) ? config.validator(control) : null; }; }; /** * JlValidators provides a collection of custom validators that execute conditionally * based on a specified condition function and Angular's built-in validators. */ const JlValidators = { required: (condition) => { return conditionalValidator({ condition: (control) => condition(control.parent), validator: Validators.required, }); }, minLength: (length, condition, watchFields) => { return conditionalValidator({ condition, validator: Validators.minLength(length) }); }, maxLength: (length, condition, watchFields) => { return conditionalValidator({ condition, validator: Validators.maxLength(length) }); }, pattern: (pattern, condition, watchFields) => { return conditionalValidator({ condition, validator: Validators.pattern(pattern) }); }, email: (condition) => { return conditionalValidator({ condition, validator: Validators.email }); }, min: (min, condition) => { return conditionalValidator({ condition, validator: Validators.min(min) }); }, max: (max, condition) => { return conditionalValidator({ condition, validator: Validators.max(max) }); }, requiredTrue: (condition) => { return conditionalValidator({ condition, validator: Validators.requiredTrue }); } }; // Definition of decorator const controlProp = (options) => { return (target, propertyKey) => { // Stores options in class (not instance) metadata const existingMetadata = Reflect.getMetadata(FORM_CONTROL_METADATA, target) || {}; existingMetadata[propertyKey] = options; Reflect.defineMetadata(FORM_CONTROL_METADATA, existingMetadata, target); }; }; /*TODO fonctionne (penser à instancier l'objet par défaut). Il faut maintenant chercher un moyen pour faire communiquer * le Formgroup parent avec le Formgroup enfant, en particulier pour les conditionnal validators */ const controlGroupProp = () => { return (target, propertyKey) => { }; }; /* * Public API Surface of typed-form-craft */ /** * Generated bundle index. Do not edit. */ export { FORM_CONTROL_METADATA, FORM_GROUP_METADATA, JlValidators, TypedFormCraftComponent, TypedFormCraftService, conditionalValidator, controlGroupProp, controlProp, createFormFromClass }; //# sourceMappingURL=jlf-typed-form-craft.mjs.map