src/app/core/admin/admin-entity-details/admin-entity-field/configure-entity-field-validator/configure-entity-field-validator.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-configure-entity-field-validator |
| standalone | true |
| imports |
MatInputModule
MatFormFieldModule
MatCheckboxModule
|
| styleUrls | ./configure-entity-field-validator.component.scss |
| templateUrl | ./configure-entity-field-validator.component.html |
| styleUrl | ./configure-entity-field-validator.component.scss |
MatInputModule
MatFormFieldModule
FormsModule
MatCheckboxModule
ReactiveFormsModule
HelpButtonComponent
EditDateComponent
EditMonthComponent
Properties |
Methods |
Inputs |
Outputs |
constructor()
|
| entitySchemaField | |
Type : EntitySchemaField
|
|
| Required : true | |
|
the field definition with the currently existing validator settings to be edited |
|
| entitySchemaFieldChanges | |
Type : Partial<EntitySchemaField>
|
|
|
Emit changes to EntitySchemaField properties (e.g. trim) that are not validators. |
|
| entityValidatorChanges | |
Type : FormValidatorConfig
|
|
|
Emit the latest state of the validators config whenever the user changed it in the displayed form. |
|
| removeDefaultValuesFromValidatorConfig | ||||||||
removeDefaultValuesFromValidatorConfig(validators: FormValidatorConfig)
|
||||||||
|
Removes default fields and returns a validator config that only contains explicitly activated validators.
Parameters :
Returns :
FormValidatorConfig
|
| isDateLikeValidatorType |
Type : unknown
|
Default value : computed(() => {
return ["date", "date-only", "date-with-age", "month"].includes(
this.entitySchemaField()?.dataType,
);
})
|
| isDateWithAgeType |
Type : unknown
|
Default value : computed(() => {
return this.entitySchemaField()?.dataType === "date-with-age";
})
|
| isMonthType |
Type : unknown
|
Default value : computed(() => {
return this.entitySchemaField()?.dataType === "month";
})
|
| isStringType |
Type : unknown
|
Default value : computed(() => {
return ["string", "long-text"].includes(this.entitySchemaField()?.dataType);
})
|
| trimControl |
Type : unknown
|
Default value : computed(
() => new FormControl(this.entitySchemaField()?.trim !== false),
)
|
import {
Component,
inject,
input,
output,
ChangeDetectionStrategy,
effect,
computed,
} from "@angular/core";
import { MatInputModule } from "@angular/material/input";
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
} from "@angular/forms";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatFormFieldModule } from "@angular/material/form-field";
import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field";
import { FormValidatorConfig } from "app/core/common-components/entity-form/dynamic-form-validators/form-validator-config";
import { HelpButtonComponent } from "../../../../common-components/help-button/help-button.component";
import { EditDateComponent } from "../../../../basic-datatypes/date/edit-date/edit-date.component";
import { EditMonthComponent } from "../../../../basic-datatypes/month/edit-month/edit-month.component";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-configure-entity-field-validator",
imports: [
MatInputModule,
MatFormFieldModule,
FormsModule,
MatCheckboxModule,
ReactiveFormsModule,
HelpButtonComponent,
EditDateComponent,
EditMonthComponent,
],
templateUrl: "./configure-entity-field-validator.component.html",
styleUrl: "./configure-entity-field-validator.component.scss",
})
export class ConfigureEntityFieldValidatorComponent {
private fb = inject(FormBuilder);
/**
* the field definition with the currently existing validator settings to be edited
*/
entitySchemaField = input.required<EntitySchemaField>();
/**
* Emit the latest state of the validators config whenever the user changed it in the displayed form.
*/
entityValidatorChanges = output<FormValidatorConfig>();
/**
* Emit changes to EntitySchemaField properties (e.g. trim) that are not validators.
*/
entitySchemaFieldChanges = output<Partial<EntitySchemaField>>();
trimControl = computed(
() => new FormControl(this.entitySchemaField()?.trim !== false),
);
validatorForm = computed(() => {
const v = this.entitySchemaField()?.validators;
const isObjectForm = typeof v?.pattern === "object";
return this.fb.group({
required: [v?.required ?? false],
min: [v?.min ?? null],
max: [v?.max ?? null],
minAge: [v?.minAge ?? null],
maxAge: [v?.maxAge ?? null],
minDate: [v?.minDate ?? null],
maxDate: [v?.maxDate ?? null],
pattern: [
(isObjectForm ? v.pattern.pattern : v?.pattern) ?? "",
validRegexValidator,
],
patternMessage: [(isObjectForm ? v.pattern.message : "") ?? ""],
uniqueId: [v?.uniqueId ?? ""],
readonlyAfterSet: [v?.readonlyAfterSet ?? false],
});
});
constructor() {
effect((onCleanup) => {
const form = this.validatorForm();
this.normalizeDateControl(form, "minDate");
this.normalizeDateControl(form, "maxDate");
this.syncPatternMessageAvailability(form);
const patternSub = form
.get("pattern")
.valueChanges.subscribe(() =>
this.syncPatternMessageAvailability(form),
);
const sub = form.valueChanges.subscribe(() => {
const rawValues = form.getRawValue();
this.transformPatternValue(rawValues);
const cleanedValues =
this.removeDefaultValuesFromValidatorConfig(rawValues);
this.entityValidatorChanges.emit(cleanedValues);
});
onCleanup(() => {
patternSub.unsubscribe();
sub.unsubscribe();
});
});
effect((onCleanup) => {
const ctrl = this.trimControl();
const sub = ctrl.valueChanges.subscribe((value: boolean) => {
this.entitySchemaFieldChanges.emit({ trim: value });
});
onCleanup(() => sub.unsubscribe());
});
effect((onCleanup) => {
const ctrl = this.trimControl();
const sub = ctrl.valueChanges.subscribe((value: boolean) => {
this.entitySchemaFieldChanges.emit({ trim: value });
});
onCleanup(() => sub.unsubscribe());
});
}
/**
* The custom validation error text is only meaningful together with a pattern,
* so the input is disabled while no pattern is entered.
*/
private syncPatternMessageAvailability(form: FormGroup) {
const messageControl = form.get("patternMessage");
if (form.get("pattern").value) {
messageControl.enable({ emitEvent: false });
} else {
messageControl.disable({ emitEvent: false });
}
}
isDateLikeValidatorType = computed(() => {
return ["date", "date-only", "date-with-age", "month"].includes(
this.entitySchemaField()?.dataType,
);
});
isStringType = computed(() => {
return ["string", "long-text"].includes(this.entitySchemaField()?.dataType);
});
/**
* Replaces the raw pattern and patternMessage inputs with a config value for the "pattern" validator:
* drops it if the pattern is empty or not a compilable regex,
* uses the object form { pattern, message } if a custom error message is entered.
*/
private transformPatternValue(
values: FormValidatorConfig & { patternMessage?: string },
) {
const newPattern = values.pattern;
const message = values.patternMessage;
delete values.patternMessage;
if (!newPattern || !isValidRegex(newPattern)) {
delete values.pattern;
return;
}
if (message) {
values.pattern = { pattern: newPattern, message };
}
}
isDateWithAgeType = computed(() => {
return this.entitySchemaField()?.dataType === "date-with-age";
});
isMonthType = computed(() => {
return this.entitySchemaField()?.dataType === "month";
});
private normalizeDateControl(form: FormGroup, controlName: string): void {
const ctrl = form.get(controlName);
const val = ctrl?.value;
if (typeof val === "string" || typeof val === "number") {
const parsed = new Date(val as any);
if (!Number.isNaN(parsed.getTime())) {
ctrl?.setValue(parsed, { emitEvent: false });
}
}
}
/**
* Removes default fields and returns a validator config that only contains explicitly activated validators.
* @param validators form values including default values that are unchanged
*/
removeDefaultValuesFromValidatorConfig(
validators: FormValidatorConfig,
): FormValidatorConfig {
for (let key of Object.keys(validators)) {
if (isDefaultValue(validators[key])) {
delete validators[key];
}
}
function isDefaultValue(value): boolean {
return value === false || value === "" || value === null;
}
return validators;
}
}
function isValidRegex(pattern: string): boolean {
try {
new RegExp(pattern);
return true;
} catch {
return false;
}
}
/**
* Marks the control as invalid if its value cannot be compiled as a regular expression.
*/
function validRegexValidator(
control: AbstractControl,
): ValidationErrors | null {
if (!control.value || isValidRegex(control.value)) {
return null;
}
return { invalidPattern: true };
}
<form class="validator-form-cell flex-column" [formGroup]="validatorForm()">
@if (entitySchemaField().dataType === "number") {
<mat-form-field class="number-field">
<mat-label i18n>Minimum Value</mat-label>
<input formControlName="min" matInput type="number" />
</mat-form-field>
<mat-form-field class="number-field">
<mat-label i18n>Maximum Value</mat-label>
<input formControlName="max" matInput type="number" />
</mat-form-field>
}
@if (isDateLikeValidatorType()) {
@if (isDateWithAgeType()) {
<mat-form-field floatLabel="always">
<mat-label i18n>Minimum Age (Years)</mat-label>
<input formControlName="minAge" matInput type="number" />
<app-help-button
matSuffix
text="Enter the minimum allowed age in years. This is independent from the date limits."
i18n-text
></app-help-button>
</mat-form-field>
<mat-form-field floatLabel="always">
<mat-label i18n>Maximum Age (Years)</mat-label>
<input formControlName="maxAge" matInput type="number" />
<app-help-button
matSuffix
text="Enter the maximum allowed age in years. This is independent from the date limits."
i18n-text
></app-help-button>
</mat-form-field>
}
@if (isMonthType()) {
<mat-form-field floatLabel="always">
<mat-label i18n>Minimum Month</mat-label>
<app-edit-month formControlName="minDate"></app-edit-month>
</mat-form-field>
<mat-form-field floatLabel="always">
<mat-label i18n>Maximum Month</mat-label>
<app-edit-month formControlName="maxDate"></app-edit-month>
</mat-form-field>
} @else {
<mat-form-field floatLabel="always">
<mat-label i18n>Minimum Date</mat-label>
<app-edit-date formControlName="minDate"></app-edit-date>
</mat-form-field>
<mat-form-field floatLabel="always">
<mat-label i18n>Maximum Date</mat-label>
<app-edit-date formControlName="maxDate"></app-edit-date>
</mat-form-field>
}
}
@if (isStringType()) {
<div class="flex-row flex-wrap gap-small">
<mat-form-field floatLabel="always" class="flex-grow">
<mat-label i18n>Pattern (Regex)</mat-label>
<input formControlName="pattern" matInput />
<app-help-button
matSuffix
text="Define a format (-> regular expression) that text values have to match to be valid."
i18n-text
></app-help-button>
<mat-error i18n>Not a valid regular expression</mat-error>
<mat-hint align="start" i18n>
Learn about regular expressions at
<a
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"
target="_blank"
rel="noopener"
>
developer.mozilla.org
</a>
</mat-hint>
</mat-form-field>
<mat-form-field floatLabel="always" class="flex-grow">
<mat-label i18n>Validation Error Text for Pattern</mat-label>
<input formControlName="patternMessage" matInput />
<app-help-button
matSuffix
text="Optional custom message shown to users when their input does not match the pattern. If empty, a generic error message is displayed."
i18n-text
></app-help-button>
<mat-hint align="start" i18n>
Shown when the entered value does not match the pattern above
</mat-hint>
</mat-form-field>
</div>
}
<div>
<mat-checkbox i18n formControlName="required">
Required Field
</mat-checkbox>
<app-help-button
text="Users always must provide a value in this field"
i18n-text
></app-help-button>
</div>
<div>
<mat-checkbox i18n formControlName="uniqueId">
Unique ID [experimental]</mat-checkbox
>
<app-help-button
text="Ensures the value is not used as an ID for another existing record already. (Experimental Feature: Does not support advanced edge cases)"
i18n-text
></app-help-button>
</div>
@if (entitySchemaField().dataType === "string") {
<div>
<mat-checkbox i18n [formControl]="trimControl()">
Trim leading/trailing spaces automatically
</mat-checkbox>
<app-help-button
text="When enabled, leading and trailing spaces are automatically removed when saving this field."
i18n-text
></app-help-button>
</div>
}
</form>
./configure-entity-field-validator.component.scss
.validator-form-cell {
width: 100%;
gap: 1rem;
}