UNPKG

@qoollo/ngx-form-url-saver

Version:

Angular directive for syncing form's data with URL query

375 lines (364 loc) 14.7 kB
import * as i0 from '@angular/core'; import { InjectionToken, forwardRef, Directive, Inject, Optional, Self, Input } from '@angular/core'; import { ControlContainer, FormGroupDirective, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms'; import { startWith, debounceTime, map } from 'rxjs'; import { DateTime } from 'luxon'; import * as i1 from '@angular/router'; class SeparatedQueryGenerationStrategy { constructor(formHandlingStrategy) { this.formHandlingStrategy = formHandlingStrategy; } inferFormValueFromQuery(queryParams, formValue) { const queryObject = {}; for (const key of Object.keys(formValue)) { queryObject[key] = this.formHandlingStrategy.parse(queryParams[key], key); } return queryObject; } convertFormValueToQueryObject(formValue) { const queryObject = {}; for (const key of Object.keys(formValue)) { queryObject[key] = this.formHandlingStrategy.stringify(formValue[key], key); } return queryObject; } createClearingObject(formValue) { const filtersObject = {}; for (const key of Object.keys(formValue)) { filtersObject[key] = null; } return filtersObject; } } class UnitedQueryGenerationStrategy { constructor(formHandlingStrategy, queryKey) { this.formHandlingStrategy = formHandlingStrategy; this.queryKey = queryKey; } inferFormValueFromQuery(queryParams) { const query = queryParams[this.queryKey]; if (!query) { return {}; } const formValue = this.formHandlingStrategy.parse(query); return formValue; } convertFormValueToQueryObject(formValue) { const serializedObject = this.formHandlingStrategy.stringify(formValue); return { [this.queryKey]: serializedObject, }; } createClearingObject() { return { [this.queryKey]: null, }; } } class DefaultFormHandlingStrategy { stringify(value) { return JSON.stringify(value); } parse(value) { if (!value) { return undefined; } return JSON.parse(value); } } const FORM_VALUE_HANDLING_TOKEN = new InjectionToken('ngx-form-url-saver'); const NGX_FORM_URL_SAVER_STRATEGY_PROVIDER = { provide: FORM_VALUE_HANDLING_TOKEN, useFactory: () => new DefaultFormHandlingStrategy(), }; /** * Checks if the given value is a valid Date object. * * @param value - The value to check. * @returns True if the value is a Date object and represents a valid date, false otherwise. * * @example * ```typescript * isDate(new Date()); // true * isDate("2025-03-12"); // false * isDate(new Date("invalid date")); // false * ``` */ function isDate(value) { return value instanceof Date && !isNaN(value.getTime()); } class SeparatedComplexQueryGenerationStrategy { constructor(useDateTime) { this.useDateTime = useDateTime; this.COMPLEX_OBJECT_PREFIX = 'complex-'; } inferFormValueFromQuery(queryParams, formValue) { const simpleQuery = this.readAllSimpleQuery(queryParams, formValue); const complexQuery = this.readAllComplexQuery(queryParams); return { ...simpleQuery, ...complexQuery }; } convertFormValueToQueryObject(formValue) { const queryObject = {}; for (const key of Object.keys(formValue)) { if (this.isObject(formValue[key]) && !DateTime.isDateTime(formValue[key])) { queryObject[this.createComplexKey(key)] = JSON.stringify(formValue[key]); } else { queryObject[key] = this.prepareValue(formValue[key]); } } return queryObject; } createClearingObject(formValue) { const filtersObject = {}; for (const key of Object.keys(formValue)) { if (this.isObject(formValue[key])) { filtersObject[this.createComplexKey(key)] = null; } else { filtersObject[key] = null; } } return filtersObject; } prepareValue(value) { if (isDate(value) || DateTime.isDateTime(value)) { return value.toJSON(); } return value; } readAllSimpleQuery(queryParams, form) { const simpleQueryObject = {}; for (const key of Object.keys(form)) { const value = this.getNewValueByQueryKey(queryParams, key, form); if (this.useDateTime && typeof value === 'string') { const dateTimeValue = DateTime.fromISO(value); simpleQueryObject[key] = dateTimeValue.isValid ? dateTimeValue : value; } else { simpleQueryObject[key] = value; } } return simpleQueryObject; } getNewValueByQueryKey(queryParams, key, form) { const queryParamValue = ['true', 'false'].includes(queryParams[key]) ? JSON.parse(queryParams[key]) : queryParams[key]; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const currentValue = form[key]; if (queryParamValue === undefined || queryParamValue === null) { return currentValue; } const queryValueMustBeConvertedToArray = Array.isArray(currentValue) && !Array.isArray(queryParamValue); return queryValueMustBeConvertedToArray ? [queryParamValue] : queryParamValue; } readAllComplexQuery(queryParams) { const complexQueryKeys = Object.keys(queryParams) .filter(paramName => this.checkIfKeyIsComplex(paramName)); if (!complexQueryKeys.length) { return {}; } const newFormValue = {}; for (const complexKey of complexQueryKeys) { const originKey = this.getOriginKey(complexKey); try { newFormValue[originKey] = JSON.parse(queryParams[complexKey]); } catch (error) { } } return newFormValue; } // #region Working with complex keys for nested objects createComplexKey(objectKey) { return this.COMPLEX_OBJECT_PREFIX + objectKey; } getOriginKey(complexKey) { return complexKey.replace(this.COMPLEX_OBJECT_PREFIX, ''); } checkIfKeyIsComplex(key) { return key.includes(this.COMPLEX_OBJECT_PREFIX); } // #endregion /** * Checks whether the passed value is an object. * Returns `false` for `date` or `array`. * * `Object.prototype.toString.call(new Date()) = [object Date]` * `Object.prototype.toString.call(new Array()) = [object Array]` */ isObject(value) { return Object.prototype.toString.call(value) === '[object Object]'; } } const formDirectiveProvider = { provide: ControlContainer, useExisting: forwardRef(() => FormUrlSaverDirective), }; /** * @description * The directive is a descendant of Angular `FormGroupDirective` * and is used to automatically write the `FormGroup` value to the query parameters. * * {@link https://github.com/angular/angular/blob/main/packages/forms/src/directives/reactive_directives/form_group_directive.ts Angular FormGroupDirective} * * Allows you to set a delay (_debounce_) for query updates. * * Allows you to select the method (_strategy_) of writing query parameters. * 'united' - the form value will be completely written in one parameter * 'separated' - each form field will be written in its own query parameter. * 'complex' - each form field will be written in its own query parameter, while saving nested objects in the form is supported. * * It is possible to override the behavior of converting the form value to a string. * Default is JSON.stringify. * Use the ValueHandlingStrategy interface and FORM_VALUE_HANDLING_TOKEN * */ class FormUrlSaverDirective extends FormGroupDirective { constructor(router, activatedRoute, cdr, formHandlingStrategy, /** * Default Angular FormGroupDirective dependencies */ _validators, _asyncValidators) { super(_validators, _asyncValidators); this.router = router; this.activatedRoute = activatedRoute; this.cdr = cdr; this.formHandlingStrategy = formHandlingStrategy; this._validators = _validators; this._asyncValidators = _asyncValidators; this.BASE_DEBOUNCE_TIME = 500; this.form = null; /** * Query parameter update delay time */ this.debounceTime = this.BASE_DEBOUNCE_TIME; /** * Strategy for creating query parameters. * * If 'united' is specified, the form value will be completely written using the key of one query parameter * * If 'separated' is specified, each form field will be written with a separate key corresponding to its name in the form * * If 'complex' is specified, each form field will be written in its own query parameter, while saving nested objects in the form is supported. */ this.strategy = 'complex'; /** * The key by which the form value will be written if the 'united' strategy is selected. */ this.queryKey = 'form'; this.useDateTime = false; } // #region Lifecycle methods ngAfterViewInit() { this.queryStrategy = this.createQueryGenerationStrategy(); this.fillFormFromQuery(); this.subscribeToFormValueChanges(); this.cdr.detectChanges(); } ngOnDestroy() { super.ngOnDestroy(); this.formValueChangedSubscription?.unsubscribe(); this.clearFormQuery(); } // #endregion getUpdatedObjectByPath(object, path, parser) { const decomposedPath = path.split('.'); const base = decomposedPath[0]; let temp; if (base === undefined || !object) { return { ...object }; } temp = decomposedPath.length <= 1 ? parser(object[base]) : this.getUpdatedObjectByPath(object[base], decomposedPath.slice(1).join('.'), parser); return { ...object, [base]: temp, }; } fillFormFromQuery() { const currentQueryParams = this.activatedRoute.snapshot.queryParams; const inferredFormValue = this.queryStrategy.inferFormValueFromQuery(currentQueryParams, this.form.value); let updatedObject = inferredFormValue; this.dataTransformMap?.forEach((parser, path) => { updatedObject = this.getUpdatedObjectByPath(updatedObject, path, parser); }); this.form.patchValue(updatedObject); this.subscribeToFormValueChanges(); } createQueryGenerationStrategy() { const strategies = { 'united': new UnitedQueryGenerationStrategy(this.formHandlingStrategy, this.queryKey), 'separated': new SeparatedQueryGenerationStrategy(this.formHandlingStrategy), 'complex': new SeparatedComplexQueryGenerationStrategy(this.useDateTime) }; return strategies[this.strategy]; } subscribeToFormValueChanges() { this.formValueChangedSubscription = this.form.valueChanges.pipe(startWith(this.form.value), debounceTime(this.debounceTime), map((value, index) => [value, index])).subscribe(([value, index]) => { void this.router.navigate([], { queryParams: this.queryStrategy.convertFormValueToQueryObject(value), queryParamsHandling: 'merge', replaceUrl: index === 0, }); }); } clearFormQuery() { setTimeout(() => { void this.router.navigate([], { queryParams: this.queryStrategy.createClearingObject(this.form.value), queryParamsHandling: 'merge', replaceUrl: true, }); }, 0); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: FormUrlSaverDirective, deps: [{ token: i1.Router }, { token: i1.ActivatedRoute }, { token: i0.ChangeDetectorRef }, { token: FORM_VALUE_HANDLING_TOKEN }, { token: NG_VALIDATORS, optional: true, self: true }, { token: NG_ASYNC_VALIDATORS, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.2", type: FormUrlSaverDirective, isStandalone: true, selector: "[ngxFormUrlSaver]", inputs: { form: ["ngxFormUrlSaver", "form"], debounceTime: "debounceTime", strategy: "strategy", queryKey: "queryKey", useDateTime: "useDateTime", dataTransformMap: "dataTransformMap" }, providers: [formDirectiveProvider, NGX_FORM_URL_SAVER_STRATEGY_PROVIDER], usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.2", ngImport: i0, type: FormUrlSaverDirective, decorators: [{ type: Directive, args: [{ selector: '[ngxFormUrlSaver]', providers: [formDirectiveProvider, NGX_FORM_URL_SAVER_STRATEGY_PROVIDER], standalone: true, }] }], ctorParameters: () => [{ type: i1.Router }, { type: i1.ActivatedRoute }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{ type: Inject, args: [FORM_VALUE_HANDLING_TOKEN] }] }, { type: Array, decorators: [{ type: Optional }, { type: Self }, { type: Inject, args: [NG_VALIDATORS] }] }, { type: Array, decorators: [{ type: Optional }, { type: Self }, { type: Inject, args: [NG_ASYNC_VALIDATORS] }] }], propDecorators: { form: [{ type: Input, args: ['ngxFormUrlSaver'] }], debounceTime: [{ type: Input }], strategy: [{ type: Input }], queryKey: [{ type: Input }], useDateTime: [{ type: Input }], dataTransformMap: [{ type: Input }] } }); /* * Public API Surface of form-url-saver-lib */ /** * Generated bundle index. Do not edit. */ export { DefaultFormHandlingStrategy, FORM_VALUE_HANDLING_TOKEN, FormUrlSaverDirective, NGX_FORM_URL_SAVER_STRATEGY_PROVIDER }; //# sourceMappingURL=qoollo-ngx-form-url-saver.mjs.map