jlf-typed-form-craft
Version:
A library to create forms with typed datas and validations
332 lines (313 loc) • 13.1 kB
JavaScript
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