@qoollo/ngx-form-url-saver
Version:
Angular directive for syncing form's data with URL query
375 lines (364 loc) • 14.7 kB
JavaScript
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