@openmfp/ngx 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ import '@ui5/webcomponents-icons/dist/add.js';
27
27
  import '@ui5/webcomponents-icons/dist/delete.js';
28
28
  import '@ui5/webcomponents-icons/dist/navigation-down-arrow.js';
29
29
  import '@ui5/webcomponents-icons/dist/navigation-right-arrow.js';
30
+ import { InputIcon } from '@fundamental-ngx/ui5-webcomponents/input-icon';
30
31
  import { Label } from '@fundamental-ngx/ui5-webcomponents/label';
31
32
  import { Option } from '@fundamental-ngx/ui5-webcomponents/option';
32
33
  import { Select } from '@fundamental-ngx/ui5-webcomponents/select';
@@ -1832,6 +1833,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
1832
1833
  ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.ShadowDom, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<ui5-dialog\n data-testid=\"generic-table-card-delete-dialog\"\n [accessibleName]=\"config()?.title ?? 'Confirm Delete'\"\n [open]=\"open()\"\n (ui5BeforeClose)=\"cancelled.emit()\"\n>\n <div class=\"dialog__header\" slot=\"header\">\n <ui5-icon class=\"dialog__header-icon\" design=\"Critical\" name=\"alert\" />\n <ui5-title level=\"H5\">\n <span\n [innerHTML]=\"config()?.title ?? 'Confirm Delete' | sanitizeHtml\"\n ></span>\n </ui5-title>\n </div>\n @if (config()?.message || requiresConfirmation()) {\n <div class=\"dialog__body\">\n @if (config()?.message; as message) {\n <p class=\"dialog__message\" [innerHTML]=\"message | sanitizeHtml\"></p>\n }\n @if (requiresConfirmation()) {\n <p class=\"dialog__message\">\n <ui5-input\n class=\"dialog__confirmation-input\"\n data-testid=\"generic-table-card-delete-confirmation-input\"\n [formControl]=\"confirmationControl\"\n [placeholder]=\"\n config()?.confirmationPlaceholder ?? 'Type to confirm'\n \"\n [valueState]=\"\n confirmationControl.invalid && confirmationControl.dirty\n ? 'Negative'\n : 'None'\n \"\n />\n </p>\n }\n </div>\n }\n <div class=\"dialog__footer\" slot=\"footer\">\n <ui5-button\n data-testid=\"generic-table-card-delete-confirm\"\n design=\"Negative\"\n [disabled]=\"confirmDisabled()\"\n (click)=\"onConfirm()\"\n >\n {{ config()?.confirmLabel ?? 'Delete' }}\n </ui5-button>\n <ui5-button\n data-testid=\"generic-table-card-delete-cancel\"\n design=\"Transparent\"\n (click)=\"cancelled.emit()\"\n >\n {{ config()?.cancelLabel ?? 'Cancel' }}\n </ui5-button>\n </div>\n</ui5-dialog>\n", styles: [":host{display:contents}.dialog__header{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem}.dialog__header-icon{color:var(--sapCriticalColor)}.dialog__body{margin:-1rem;min-width:320px;padding:1rem}.dialog__message{margin:0;font-size:.875rem;max-width:320px}.dialog__message--critical{color:var(--sapCriticalElementColor)}.dialog__confirmation-input{display:block;width:100%;max-width:320px}.dialog__footer{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.5rem 1rem;margin:0 -1rem;width:100%}\n"] }]
1833
1834
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], confirmed: [{ type: i0.Output, args: ["confirmed"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
1834
1835
 
1836
+ function coerceBoolean(value) {
1837
+ if (typeof value === 'boolean')
1838
+ return value;
1839
+ if (typeof value === 'string') {
1840
+ return value.trim().toLowerCase() === 'true';
1841
+ }
1842
+ return false;
1843
+ }
1844
+
1835
1845
  const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
1836
1846
  function setPropertyByPath(object, path, value) {
1837
1847
  const segments = path.split('.').filter(Boolean);
@@ -1941,6 +1951,8 @@ class DeclarativeForm {
1941
1951
  form;
1942
1952
  collectionSeeds = signal({}, /* @ts-ignore */
1943
1953
  ...(ngDevMode ? [{ debugName: "collectionSeeds" }] : /* istanbul ignore next */ []));
1954
+ passwordVisible = signal({}, /* @ts-ignore */
1955
+ ...(ngDevMode ? [{ debugName: "passwordVisible" }] : /* istanbul ignore next */ []));
1944
1956
  fb = inject(FormBuilder);
1945
1957
  constructor() {
1946
1958
  this.form = this.fb.group({});
@@ -1977,6 +1989,47 @@ class DeclarativeForm {
1977
1989
  });
1978
1990
  }
1979
1991
  }
1992
+ setSwitchValue($event, field) {
1993
+ const target = $event.target;
1994
+ const control = this.form.controls[field.name];
1995
+ const checked = coerceBoolean(target.checked);
1996
+ control.setValue(checked);
1997
+ control.markAsTouched();
1998
+ control.markAsDirty();
1999
+ this.formValueChange.emit(this.form.value);
2000
+ if (field.validation === 'onChange') {
2001
+ this.fieldChange.emit({
2002
+ fieldProperty: field.name,
2003
+ value: checked,
2004
+ });
2005
+ }
2006
+ }
2007
+ coerceChecked = coerceBoolean;
2008
+ isPasswordVisible(field) {
2009
+ return this.passwordVisible()[field.name] ?? false;
2010
+ }
2011
+ passwordInputType(field) {
2012
+ if (field.inputType !== 'Password') {
2013
+ return 'Text';
2014
+ }
2015
+ return this.isPasswordVisible(field) ? 'Text' : 'Password';
2016
+ }
2017
+ passwordToggleIcon(field) {
2018
+ return this.isPasswordVisible(field) ? 'hide' : 'show';
2019
+ }
2020
+ passwordToggleLabel(field) {
2021
+ return this.isPasswordVisible(field) ? 'Hide value' : 'Show value';
2022
+ }
2023
+ shouldShowPasswordToggle(field) {
2024
+ return field.inputType === 'Password' && (field.showPasswordToggle ?? true);
2025
+ }
2026
+ togglePasswordVisibility(field, event) {
2027
+ event.stopPropagation();
2028
+ this.passwordVisible.update((state) => ({
2029
+ ...state,
2030
+ [field.name]: !this.isPasswordVisible(field),
2031
+ }));
2032
+ }
1980
2033
  onCollectionValueChange(field, entries) {
1981
2034
  const array = this.form.controls[field.name];
1982
2035
  if (!array)
@@ -2017,6 +2070,7 @@ class DeclarativeForm {
2017
2070
  }
2018
2071
  clear() {
2019
2072
  this.form.reset();
2073
+ this.passwordVisible.set({});
2020
2074
  }
2021
2075
  collectionEntries(field) {
2022
2076
  return this.collectionSeeds()[field.name] ?? [];
@@ -2039,6 +2093,9 @@ class DeclarativeForm {
2039
2093
  if (wantsCollection) {
2040
2094
  this.form.addControl(field.name, this.fb.array([]));
2041
2095
  }
2096
+ else if (field.inputType === 'Switch') {
2097
+ this.form.addControl(field.name, new FormControl(false));
2098
+ }
2042
2099
  else {
2043
2100
  this.form.addControl(field.name, new FormControl(''));
2044
2101
  }
@@ -2073,7 +2130,13 @@ class DeclarativeForm {
2073
2130
  seeds[field.name] = entries;
2074
2131
  }
2075
2132
  this.collectionSeeds.set(seeds);
2076
- this.form.patchValue(initialValues, { emitEvent: false });
2133
+ const normalizedValues = { ...initialValues };
2134
+ for (const field of this.fields()) {
2135
+ if (field.inputType === 'Switch') {
2136
+ normalizedValues[field.name] = coerceBoolean(normalizedValues[field.name]);
2137
+ }
2138
+ }
2139
+ this.form.patchValue(normalizedValues, { emitEvent: false });
2077
2140
  this.form.markAsPristine({ emitEvent: false });
2078
2141
  this.form.updateValueAndValidity({ emitEvent: false });
2079
2142
  }
@@ -2095,18 +2158,20 @@ class DeclarativeForm {
2095
2158
  });
2096
2159
  }
2097
2160
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
2098
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: DeclarativeForm, isStandalone: true, selector: "mfp-declarative-form", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, initialValues: { classPropertyName: "initialValues", publicName: "initialValues", isSignal: true, isRequired: false, transformFunction: null }, fieldErrors: { classPropertyName: "fieldErrors", publicName: "fieldErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChange: "fieldChange", formSubmit: "formSubmit", formValueChange: "formValueChange" }, ngImport: i0, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.collection-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: Input, selector: "ui5-input, [ui5-input]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "filter", "maxlength", "name", "noTypeahead", "open", "placeholder", "readonly", "required", "showClearIcon", "showSuggestions", "type", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5Input", "ui5Open", "ui5Select", "ui5SelectionChange"], exportAs: ["ui5Input"] }, { kind: "component", type: Label, selector: "ui5-label, [ui5-label]", inputs: ["for", "required", "showColon", "wrappingType"], exportAs: ["ui5Label"] }, { kind: "component", type: Select, selector: "ui5-select, [ui5-select]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "name", "readonly", "required", "textSeparator", "tooltip", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5LiveChange", "ui5Open"], exportAs: ["ui5Select"] }, { kind: "component", type: Option, selector: "ui5-option, [ui5-option]", inputs: ["additionalText", "icon", "selected", "tooltip", "value"], outputs: ["ui5Click"], exportAs: ["ui5Option"] }, { kind: "component", type: FormCollectionField, selector: "mfp-form-collection-field", inputs: ["fields", "label", "initialEntries"], outputs: ["valueChange"] }], encapsulation: i0.ViewEncapsulation.ShadowDom });
2161
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: DeclarativeForm, isStandalone: true, selector: "mfp-declarative-form", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, initialValues: { classPropertyName: "initialValues", publicName: "initialValues", isSignal: true, isRequired: false, transformFunction: null }, fieldErrors: { classPropertyName: "fieldErrors", publicName: "fieldErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChange: "fieldChange", formSubmit: "formSubmit", formValueChange: "formValueChange" }, ngImport: i0, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n [class.inputs--switch]=\"field.inputType === 'Switch'\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"\n field.inputType === 'Switch'\n ? null\n : 'generic-form-field-input-' + field.name\n \"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else if (field.inputType === 'Switch') {\n <ui5-switch\n class=\"switch\"\n [accessibleName]=\"field.label\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [checked]=\"coerceChecked(control.value)\"\n [disabled]=\"field.disabled\"\n (blur)=\"onFieldBlur(field)\"\n (ui5Change)=\"setSwitchValue($event, field)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"field-error\" role=\"alert\">{{ error }}</div>\n }\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [attr.title]=\"field.placeholder || null\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [placeholder]=\"field.placeholder ?? ''\"\n [required]=\"field.required\"\n [type]=\"passwordInputType(field)\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (shouldShowPasswordToggle(field)) {\n <ui5-input-icon\n slot=\"icon\"\n [accessibleName]=\"passwordToggleLabel(field)\"\n [attr.data-testid]=\"\n 'generic-form-field-' + field.name + '-password-toggle'\n \"\n [name]=\"passwordToggleIcon(field)\"\n (ui5Click)=\"togglePasswordVisibility(field, $event)\"\n />\n }\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n @if (field.hint && !field.propertyCollection?.length) {\n <div\n class=\"field-hint\"\n [attr.data-testid]=\"'generic-form-field-hint-' + field.name\"\n >\n {{ field.hint }}\n </div>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.inputs--switch{flex-direction:row;align-items:center;gap:.75rem}.switch{flex-shrink:0}.inputs--switch .field-hint{flex-basis:100%}.collection-error,.field-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}.inputs--switch .field-error{flex-basis:100%}.field-hint{color:var(--sapContent_LabelColor, #6a6d70);font-size:.75rem;line-height:1.4;margin-top:.25rem;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: Input, selector: "ui5-input, [ui5-input]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "filter", "maxlength", "name", "noTypeahead", "open", "placeholder", "readonly", "required", "showClearIcon", "showSuggestions", "type", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5Input", "ui5Open", "ui5Select", "ui5SelectionChange"], exportAs: ["ui5Input"] }, { kind: "component", type: InputIcon, selector: "ui5-input-icon, [ui5-input-icon]", inputs: ["accessibleName", "name"], outputs: ["ui5Click"], exportAs: ["ui5InputIcon"] }, { kind: "component", type: Label, selector: "ui5-label, [ui5-label]", inputs: ["for", "required", "showColon", "wrappingType"], exportAs: ["ui5Label"] }, { kind: "component", type: Select, selector: "ui5-select, [ui5-select]", inputs: ["accessibleDescription", "accessibleDescriptionRef", "accessibleName", "accessibleNameRef", "disabled", "name", "readonly", "required", "textSeparator", "tooltip", "value", "valueState"], outputs: ["ui5Change", "ui5Close", "ui5LiveChange", "ui5Open"], exportAs: ["ui5Select"] }, { kind: "component", type: Option, selector: "ui5-option, [ui5-option]", inputs: ["additionalText", "icon", "selected", "tooltip", "value"], outputs: ["ui5Click"], exportAs: ["ui5Option"] }, { kind: "component", type: Switch, selector: "ui5-switch, [ui5-switch]", inputs: ["accessibleName", "accessibleNameRef", "checked", "design", "disabled", "name", "readonly", "required", "textOff", "textOn", "tooltip", "value"], outputs: ["ui5Change"], exportAs: ["ui5Switch"] }, { kind: "component", type: FormCollectionField, selector: "mfp-form-collection-field", inputs: ["fields", "label", "initialEntries"], outputs: ["valueChange"] }], encapsulation: i0.ViewEncapsulation.ShadowDom });
2099
2162
  }
2100
2163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, decorators: [{
2101
2164
  type: Component,
2102
2165
  args: [{ selector: 'mfp-declarative-form', imports: [
2103
2166
  ReactiveFormsModule,
2104
2167
  Input,
2168
+ InputIcon,
2105
2169
  Label,
2106
2170
  Select,
2107
2171
  Option,
2172
+ Switch,
2108
2173
  FormCollectionField,
2109
- ], encapsulation: ViewEncapsulation.ShadowDom, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.collection-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}\n"] }]
2174
+ ], encapsulation: ViewEncapsulation.ShadowDom, template: "<section class=\"form\" data-testid=\"generic-form\" [formGroup]=\"form\">\n @for (field of fields(); track field.name) {\n @let control = form.controls[field.name];\n <div\n class=\"inputs\"\n [attr.data-testid]=\"'generic-form-field-container-' + field.name\"\n [class.inputs--switch]=\"field.inputType === 'Switch'\"\n >\n <ui5-label\n show-colon\n [attr.data-testid]=\"'generic-form-field-label-' + field.name\"\n [attr.for]=\"\n field.inputType === 'Switch'\n ? null\n : 'generic-form-field-input-' + field.name\n \"\n [required]=\"field.required\"\n >{{ field.label }}</ui5-label\n >\n @if (field.propertyCollection?.length) {\n <mfp-form-collection-field\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [fields]=\"field.propertyCollection!\"\n [initialEntries]=\"collectionEntries(field)\"\n [label]=\"field.label\"\n (valueChange)=\"onCollectionValueChange(field, $event)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"collection-error\" role=\"alert\">{{ error }}</div>\n }\n } @else if (field.values?.length) {\n <ui5-select\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [required]=\"field.required\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n @for (value of [''].concat(field.values ?? []); track value) {\n <ui5-option\n [attr.data-testid]=\"\n 'generic-form-field-' +\n field.name +\n '-option-' +\n (value || 'empty')\n \"\n [selected]=\"value === control.value\"\n [value]=\"value\"\n >{{ value }}</ui5-option\n >\n }\n </ui5-select>\n } @else if (field.inputType === 'Switch') {\n <ui5-switch\n class=\"switch\"\n [accessibleName]=\"field.label\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [checked]=\"coerceChecked(control.value)\"\n [disabled]=\"field.disabled\"\n (blur)=\"onFieldBlur(field)\"\n (ui5Change)=\"setSwitchValue($event, field)\"\n />\n @if (getError(field.name); as error) {\n <div class=\"field-error\" role=\"alert\">{{ error }}</div>\n }\n } @else {\n <ui5-input\n class=\"input\"\n [attr.data-testid]=\"'generic-form-field-' + field.name\"\n [attr.title]=\"field.placeholder || null\"\n [disabled]=\"field.disabled\"\n [id]=\"'generic-form-field-input-' + field.name\"\n [placeholder]=\"field.placeholder ?? ''\"\n [required]=\"field.required\"\n [type]=\"passwordInputType(field)\"\n [value]=\"control.value\"\n [valueState]=\"getValueState(field.name)\"\n (blur)=\"onFieldBlur(field)\"\n (change)=\"setFormControlValue($event, field)\"\n (input)=\"setFormControlValue($event, field)\"\n >\n @if (shouldShowPasswordToggle(field)) {\n <ui5-input-icon\n slot=\"icon\"\n [accessibleName]=\"passwordToggleLabel(field)\"\n [attr.data-testid]=\"\n 'generic-form-field-' + field.name + '-password-toggle'\n \"\n [name]=\"passwordToggleIcon(field)\"\n (ui5Click)=\"togglePasswordVisibility(field, $event)\"\n />\n }\n @if (getError(field.name); as error) {\n <div slot=\"valueStateMessage\">{{ error }}</div>\n }\n </ui5-input>\n }\n @if (field.hint && !field.propertyCollection?.length) {\n <div\n class=\"field-hint\"\n [attr.data-testid]=\"'generic-form-field-hint-' + field.name\"\n >\n {{ field.hint }}\n </div>\n }\n </div>\n }\n</section>\n", styles: [".form>div{display:flex;flex-direction:column;justify-content:space-evenly;align-items:flex-start;margin-bottom:.5rem;width:100%}.input{width:100%}.inputs--switch{flex-direction:row;align-items:center;gap:.75rem}.switch{flex-shrink:0}.inputs--switch .field-hint{flex-basis:100%}.collection-error,.field-error{color:var(--sapNegativeColor, #b00);font-size:.75rem;margin-top:.25rem}.inputs--switch .field-error{flex-basis:100%}.field-hint{color:var(--sapContent_LabelColor, #6a6d70);font-size:.75rem;line-height:1.4;margin-top:.25rem;width:100%}\n"] }]
2110
2175
  }], ctorParameters: () => [], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: false }] }], initialValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValues", required: false }] }], fieldErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldErrors", required: false }] }], fieldChange: [{ type: i0.Output, args: ["fieldChange"] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], formValueChange: [{ type: i0.Output, args: ["formValueChange"] }] } });
2111
2176
 
2112
2177
  class ResourceFormDialog {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmfp/ngx",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -526,6 +526,22 @@ interface FormFieldDefinition {
526
526
  values?: string[];
527
527
  /** When `true`, the field is disabled (not interactive). */
528
528
  disabled?: boolean;
529
+ /** UI5 input type rendered by `mfp-declarative-form`. */
530
+ inputType?: 'Text' | 'Password' | 'Switch';
531
+ /**
532
+ * When `inputType` is `Password`, renders a show/hide icon inside the input.
533
+ * Defaults to `true`; set to `false` to keep the field masked without a toggle.
534
+ */
535
+ showPasswordToggle?: boolean;
536
+ /** UI5 input placeholder (functional, not example values). */
537
+ placeholder?: string;
538
+ /** Persistent help text below the control; never submitted with the form. */
539
+ hint?: string;
540
+ /**
541
+ * When `true`, the host should omit this field from read queries and skip
542
+ * empty values on edit submit (e.g. write-only secrets).
543
+ */
544
+ writeOnly?: boolean;
529
545
  /** Controls when `fieldChange` is emitted for this field. When omitted, `fieldChange` is never emitted. */
530
546
  validation?: 'onBlur' | 'onChange';
531
547
  /**
@@ -553,6 +569,8 @@ interface FormFieldChangeEvent {
553
569
  /** Map of field names to their current validation error messages (`null` = no error). */
554
570
  type FormFieldErrors = Record<string, string | null>;
555
571
 
572
+ declare function coerceBoolean(value: unknown): boolean;
573
+
556
574
  declare class DeclarativeForm<T extends GenericResource> {
557
575
  readonly fields: _angular_core.InputSignal<FormFieldDefinition[]>;
558
576
  readonly initialValues: _angular_core.InputSignal<T>;
@@ -562,9 +580,18 @@ declare class DeclarativeForm<T extends GenericResource> {
562
580
  readonly formValueChange: _angular_core.OutputEmitterRef<Record<string, unknown>>;
563
581
  readonly form: FormGroup;
564
582
  protected readonly collectionSeeds: _angular_core.WritableSignal<Record<string, Record<string, unknown>[]>>;
583
+ private readonly passwordVisible;
565
584
  private readonly fb;
566
585
  constructor();
567
586
  setFormControlValue($event: Event, field: FormFieldDefinition): void;
587
+ setSwitchValue($event: Event, field: FormFieldDefinition): void;
588
+ protected readonly coerceChecked: typeof coerceBoolean;
589
+ isPasswordVisible(field: FormFieldDefinition): boolean;
590
+ passwordInputType(field: FormFieldDefinition): 'Text' | 'Password';
591
+ passwordToggleIcon(field: FormFieldDefinition): 'hide' | 'show';
592
+ passwordToggleLabel(field: FormFieldDefinition): string;
593
+ shouldShowPasswordToggle(field: FormFieldDefinition): boolean;
594
+ togglePasswordVisibility(field: FormFieldDefinition, event: Event): void;
568
595
  onCollectionValueChange(field: FormFieldDefinition, entries: Record<string, unknown>[]): void;
569
596
  getError(name: string): string | null;
570
597
  getValueState(name: string): 'None' | 'Negative';