Skip to content

Commit

Permalink
Merge pull request #139 from erqk/v8
Browse files Browse the repository at this point in the history
V8
  • Loading branch information
erqk authored Aug 20, 2024
2 parents 40d56f1 + 32b907e commit 64d3560
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 176 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# 8.2.5 (2024-08-20)

[802c219]: https://github.com/erqk/ng-dynamic-json-form/commit/802c2190f4ea5a09a15fdafd6c2c3e4df11eb3a8
[18ff62f]: https://github.com/erqk/ng-dynamic-json-form/commit/18ff62fdc4ca4c6fa499605f885bf1c326e58881
[38ad2ac]: https://github.com/erqk/ng-dynamic-json-form/commit/38ad2ac75c51846e842fe34bc9842dd58c788770

| Commit | Type | Description |
| --------- | ---- | ---------------------------------------------------------- |
| [802c219] | feat | Support custom validator with value. |
| [18ff62f] | fix | Conditions for validators will only works on the last one. |
| [38ad2ac] | fix | Custom action failed to execute. |

# 8.2.4 (2024-08-15)

[6d7c9c9]: https://github.com/erqk/ng-dynamic-json-form/commit/6d7c9c9a3b197ae98c7e93e022acdba273045e02

| Commit | Type | Description |
| --------- | ---- | ----------------------------------------- |
| [6d7c9c9] | fix | The comparator is compare the same thing. |

# 8.2.3 (2024-08-10)

[b30179a]: https://github.com/erqk/ng-dynamic-json-form/commit/b30179acac30f2df794793391c03e92b6bf0d866

| Commit | Type | Description |
| --------- | ---- | ----------------------------------------------------- |
| [b30179a] | fix | Use `CSS.escape()` to escape all invalid charactersw. |

# 8.2.2 (2024-08-06)

[32a14cf]: https://github.com/erqk/ng-dynamic-json-form/commit/32a14cfbf2d1195968b22f201b38f2ac1c53aa68
Expand Down
4 changes: 3 additions & 1 deletion lib/core/models/custom-validators.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ValidatorFn } from '@angular/forms';

export type CustomValidators = { [key: string]: ValidatorFn };
export type CustomValidators = {
[key: string]: ValidatorFn | ((_: any) => ValidatorFn);
};
11 changes: 9 additions & 2 deletions lib/core/services/config-mapping.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ beforeAll(() => {

describe('Set fallback value depends on type', () => {
it('Checkbox value should becomes false when it is undefined', () => {
const result = service['_getFallbackValue'](undefined, 'checkbox');
const result = service['_getFallbackValue']({
formControlName: 'test',
type: 'checkbox',
});
expect(result).toBe(false);
});

it('Switch value should becomes false when it is undefined', () => {
const result = service['_getFallbackValue'](null, 'switch');
const result = service['_getFallbackValue']({
formControlName: 'test',
type: 'switch',
value: null,
});
expect(result).toBe(false);
});
});
Expand Down
236 changes: 147 additions & 89 deletions lib/core/services/form-conditions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, RendererFactory2, inject } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import {
Observable,
debounceTime,
distinctUntilChanged,
filter,
from,
mergeMap,
Expand All @@ -11,6 +11,7 @@ import {
tap,
} from 'rxjs';
import {
Conditions,
ConditionsActionEnum,
ConditionsGroup,
ConditionsStatementTupple,
Expand All @@ -29,7 +30,13 @@ export class FormConditionsService {
private _renderer2 = inject(RendererFactory2).createRenderer(null, null);
private _globalVariableService = inject(GlobalVariableService);
private _formValidationService = inject(FormValidationService);
private _pauseEvent = false;
private _lastExecutedAction: {
[controlPath: string]: {
validatorConfigs?: ValidatorConfig[];
disabled?: boolean;
hidden?: boolean;
};
} = {};

/**Listen to the controls that specified in `conditions` to trigger the `targetControl` status and validators
* @param form The root form
Expand All @@ -48,11 +55,14 @@ export class FormConditionsService {
.filter(Boolean) as AbstractControl[];

const configsWithConditions = this._configsWithConditions(configs);
const valueChanges$ = (c: AbstractControl) =>
c.valueChanges.pipe(
startWith(c.value),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
);

return from(controls).pipe(
mergeMap((x) => x.valueChanges.pipe(startWith(x.value))),
filter(() => !this._pauseEvent),
debounceTime(0),
mergeMap((x) => valueChanges$(x)),
tap(() => this._onConditionsMet(configsWithConditions))
);
}
Expand All @@ -69,111 +79,159 @@ export class FormConditionsService {

const config = data[key];
const conditions = config.conditions!;
const actions = Object.keys(conditions) as ConditionsActionEnum[];

for (const action of actions) {
const bool = this._evaluateConditionsStatement(conditions[action]!);
if (bool === undefined) return;
this._executeCustomActions(conditions, control);
this._toggleValidators(config, control, key);
this._toggleControlStates(conditions, control, key);
}
}

const isPredefinedAction =
Object.values(ConditionsActionEnum).includes(action);
const toggleState = new RegExp(/^control\.[a-zA-Z]{1,}$/).test(action);
const toggleValidators = new RegExp(/^validator\.[a-zA-z]{1,}$/).test(
action
);
private _toggleControlStates(
conditions: Conditions,
control: AbstractControl,
controlPath: string
): void {
const actionDisabled = ConditionsActionEnum['control.disabled'];
const actionHidden = ConditionsActionEnum['control.hidden'];
const actions = Object.keys(conditions).filter(
(x) => x === actionDisabled || x === actionHidden
);

if (isPredefinedAction) {
if (toggleState) {
this._toggleControlState({
action,
bool,
control,
controlPath: key,
});
}

if (toggleValidators) {
this._toggleValidators({
action,
bool,
control,
validatorConfigs: config.validators ?? [],
});
}
} else if (bool) {
const functions =
this._globalVariableService.conditionsActionFuntions;

if (!functions) return;
if (!functions[action]) return;
if (typeof functions[action] !== 'function') return;

functions[action](control);
}
}
if (!actions.length) {
return;
}
}

private _toggleControlState(data: {
action: ConditionsActionEnum;
bool: boolean;
control: AbstractControl;
controlPath: string;
}): void {
const { action, bool, control, controlPath } = data;
const toggleDisabled = (disabled: boolean) => {
this._pauseEvent = true;
disabled ? control.disable() : control.enable();
this._pauseEvent = false;
};

switch (action) {
case ConditionsActionEnum['control.disabled']:
toggleDisabled(bool);
break;

case ConditionsActionEnum['control.hidden']:
toggleDisabled(bool);

this._getTargetEl$(controlPath)
.pipe(
filter(Boolean),
tap((x) => {
this._renderer2.setStyle(x, 'display', bool ? 'none' : null);
})
)
.subscribe();
break;
const disableControl = (bool: boolean) => {
const noChange = this._getLastAction(controlPath, 'disabled') === bool;
if (noChange) return;

this._setLastAction(controlPath, 'disabled', bool);
toggleDisabled(bool);
};

const hideControl = (bool: boolean) => {
const noChange = this._getLastAction(controlPath, 'hidden') === bool;
if (noChange) return;

this._setLastAction(controlPath, 'hidden', bool);
this._getTargetEl$(controlPath)
.pipe(
filter(Boolean),
tap((x) => {
toggleDisabled(bool);
this._renderer2.setStyle(x, 'display', bool ? 'none' : null);
})
)
.subscribe();
};

for (const action of actions) {
const bool = this._evaluateConditionsStatement(conditions[action]!);
if (bool === undefined) return;
if (action === actionDisabled) disableControl(bool);
if (action === actionHidden) hideControl(bool);
}
}

private _toggleValidators(data: {
action: ConditionsActionEnum;
bool: boolean;
control: AbstractControl;
validatorConfigs: ValidatorConfig[];
}): void {
const { action, bool, control, validatorConfigs } = data;
const _validatorConfigs = bool
? validatorConfigs
: validatorConfigs.filter(
(x) => x.name !== action.replace('validator.', '')
);
private _toggleValidators(
config: FormControlConfig,
control: AbstractControl,
controlPath: string
): void {
const { conditions = {}, validators = [] } = config;
const actions = Object.keys(conditions).filter((x) =>
new RegExp(/^validator\.[a-zA-z]{1,}$/).test(x)
);

if (!actions.length) {
return;
}

const validatorConfigs = validators.filter((x) => {
const actionName = `validator.${x.name ?? ''}` as ConditionsActionEnum;
const target = actions.includes(actionName);

return !target
? false
: this._evaluateConditionsStatement(conditions[actionName]!);
});

const prevConfigs = this._getLastAction(controlPath, 'validatorConfigs');
const noChange =
JSON.stringify(prevConfigs) === JSON.stringify(validatorConfigs);

if (!noChange) {
const resultValidators =
this._formValidationService.getValidators(validatorConfigs);

control.setValidators(resultValidators);
control.updateValueAndValidity();

this._setLastAction(controlPath, 'validatorConfigs', validatorConfigs);
}
}

private _executeCustomActions(
conditions: Conditions,
control: AbstractControl
): void {
const definedActions = Object.values(ConditionsActionEnum);
const customActions = Object.keys(conditions).filter(
(x) => !definedActions.includes(x as ConditionsActionEnum)
);

if (!customActions.length) {
return;
}

const validators =
this._formValidationService.getValidators(_validatorConfigs);
for (const action of customActions) {
const bool = this._evaluateConditionsStatement(conditions[action]!);
if (!bool) return;

const functions = this._globalVariableService.conditionsActionFuntions;

if (!functions) return;
if (!functions[action]) return;
if (typeof functions[action] !== 'function') return;

functions[action](control);
}
}

private _setLastAction(
key: string,
type: keyof (typeof this._lastExecutedAction)[''],
value: boolean | ValidatorConfig[]
): void {
const target = this._lastExecutedAction[key];

if (!target) {
Object.assign(this._lastExecutedAction, { [key]: { [type]: value } });
return;
}

Object.assign(target, { [type]: value });
}

control.setValidators(validators);
control.updateValueAndValidity();
private _getLastAction(
key: string,
type: keyof (typeof this._lastExecutedAction)['']
): boolean | ValidatorConfig[] | undefined {
const target = this._lastExecutedAction[key];
return !target ? undefined : target[type];
}

/**Get the target element by using `id`(full control path) on each `div` inside current NgDynamicJsonForm instance */
private _getTargetEl$(controlPath: string): Observable<HTMLElement | null> {
return new Observable((subscriber) => {
window.requestAnimationFrame(() => {
// Must escape the "." character so that querySelector will work correctly
// Use `CSS.escape()` to escape all the invalid characters.
const element = this._globalVariableService.hostElement?.querySelector(
`#${controlPath.replaceAll('.', '\\.')}`
`#${CSS.escape(controlPath)}`
);

subscriber.next(!element ? null : (element as HTMLElement));
Expand Down
Loading

0 comments on commit 64d3560

Please sign in to comment.