@openmfp/ngx 1.1.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';
@@ -1232,7 +1233,6 @@ class Dashboard {
1232
1233
  ...(ngDevMode ? [{ debugName: "isDashboardEditable" }] : /* istanbul ignore next */ []));
1233
1234
  gridOptions = computed(() => ({
1234
1235
  cellHeight: CELL_HEIGHT,
1235
- sizeToContent: true,
1236
1236
  disableResize: !this.editMode(),
1237
1237
  disableDrag: !this.editMode(),
1238
1238
  marginBottom: 0,
@@ -1833,6 +1833,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
1833
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"] }]
1834
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"] }] } });
1835
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
+
1836
1845
  const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
1837
1846
  function setPropertyByPath(object, path, value) {
1838
1847
  const segments = path.split('.').filter(Boolean);
@@ -1942,6 +1951,8 @@ class DeclarativeForm {
1942
1951
  form;
1943
1952
  collectionSeeds = signal({}, /* @ts-ignore */
1944
1953
  ...(ngDevMode ? [{ debugName: "collectionSeeds" }] : /* istanbul ignore next */ []));
1954
+ passwordVisible = signal({}, /* @ts-ignore */
1955
+ ...(ngDevMode ? [{ debugName: "passwordVisible" }] : /* istanbul ignore next */ []));
1945
1956
  fb = inject(FormBuilder);
1946
1957
  constructor() {
1947
1958
  this.form = this.fb.group({});
@@ -1978,6 +1989,47 @@ class DeclarativeForm {
1978
1989
  });
1979
1990
  }
1980
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
+ }
1981
2033
  onCollectionValueChange(field, entries) {
1982
2034
  const array = this.form.controls[field.name];
1983
2035
  if (!array)
@@ -2018,6 +2070,7 @@ class DeclarativeForm {
2018
2070
  }
2019
2071
  clear() {
2020
2072
  this.form.reset();
2073
+ this.passwordVisible.set({});
2021
2074
  }
2022
2075
  collectionEntries(field) {
2023
2076
  return this.collectionSeeds()[field.name] ?? [];
@@ -2040,6 +2093,9 @@ class DeclarativeForm {
2040
2093
  if (wantsCollection) {
2041
2094
  this.form.addControl(field.name, this.fb.array([]));
2042
2095
  }
2096
+ else if (field.inputType === 'Switch') {
2097
+ this.form.addControl(field.name, new FormControl(false));
2098
+ }
2043
2099
  else {
2044
2100
  this.form.addControl(field.name, new FormControl(''));
2045
2101
  }
@@ -2074,7 +2130,13 @@ class DeclarativeForm {
2074
2130
  seeds[field.name] = entries;
2075
2131
  }
2076
2132
  this.collectionSeeds.set(seeds);
2077
- 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 });
2078
2140
  this.form.markAsPristine({ emitEvent: false });
2079
2141
  this.form.updateValueAndValidity({ emitEvent: false });
2080
2142
  }
@@ -2096,18 +2158,20 @@ class DeclarativeForm {
2096
2158
  });
2097
2159
  }
2098
2160
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
2099
- 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 });
2100
2162
  }
2101
2163
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: DeclarativeForm, decorators: [{
2102
2164
  type: Component,
2103
2165
  args: [{ selector: 'mfp-declarative-form', imports: [
2104
2166
  ReactiveFormsModule,
2105
2167
  Input,
2168
+ InputIcon,
2106
2169
  Label,
2107
2170
  Select,
2108
2171
  Option,
2172
+ Switch,
2109
2173
  FormCollectionField,
2110
- ], 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"] }]
2111
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"] }] } });
2112
2176
 
2113
2177
  class ResourceFormDialog {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmfp/ngx",
3
- "version": "1.1.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';