Skip to content

Commit f1312ba

Browse files
committed
feat(form): support Angular's signal forms with new si-form-field
Introduces a new component called `si-form-field` which is built around Angular's signal forms. This initial version only supports native browser inputs. Currently missing: - integration of the error resolution mechanism - label in a separate column - fieldset combination with fieldset - documentation
1 parent 48815b1 commit f1312ba

16 files changed

Lines changed: 585 additions & 0 deletions

api-goldens/element-ng/form/index.api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AfterContentChecked } from '@angular/core';
99
import { AfterContentInit } from '@angular/core';
1010
import * as _angular_core from '@angular/core';
1111
import * as _angular_forms from '@angular/forms';
12+
import * as _angular_forms_signals from '@angular/forms/signals';
1213
import { DoCheck } from '@angular/core';
1314
import { ElementRef } from '@angular/core';
1415
import { FormGroup } from '@angular/forms';
@@ -21,12 +22,16 @@ import { OnInit } from '@angular/core';
2122
import { Provider } from '@angular/core';
2223
import { RequiredValidator } from '@angular/forms';
2324
import { Signal } from '@angular/core';
25+
import { SignalFormsConfig } from '@angular/forms/signals';
2426
import { SimpleChanges } from '@angular/core';
2527
import { TranslatableString } from '@siemens/element-translate-ng/translate';
2628

2729
// @public
2830
export const provideFormValidationErrorMapper: (mapper: SiFormValidationErrorMapper) => Provider;
2931

32+
// @public
33+
export const provideSiFormFieldConfig: (config?: SignalFormsConfig) => Provider[];
34+
3035
// @public
3136
export const SI_FORM_ITEM_CONTROL: InjectionToken<SiFormItemControl>;
3237

@@ -47,6 +52,12 @@ export class SiFormContainerComponent<TControl extends {
4752
get validFormContainerMessage(): boolean;
4853
}
4954

55+
// @public
56+
export class SiFormFieldComponent {
57+
constructor();
58+
readonly label: _angular_core.InputSignal<TranslatableString>;
59+
}
60+
5061
// @public (undocumented)
5162
export class SiFormFieldsetComponent implements DoCheck {
5263
readonly inline: _angular_core.InputSignalWithTransform<boolean, unknown>;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright (c) Siemens 2016 - 2026
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
import { expect, test } from '../../support/test-helpers';
6+
7+
test.describe('si-signal-form', () => {
8+
const example = 'si-signal-form/si-signal-form';
9+
10+
test(example, async ({ page, si }) => {
11+
await si.visitExample(example, false);
12+
await si.runVisualAndA11yTests(undefined);
13+
14+
// fill the name with an invalid value
15+
const name = page.getByLabel('Name', { exact: true });
16+
await name.fill('a');
17+
18+
await page.getByText('Save').click();
19+
20+
await si.runVisualAndA11yTests('validated');
21+
22+
// verify that the angular provided error messages are linked to the controls
23+
await expect(await si.getDescription(name)).toHaveText(
24+
'Minimum 3 characters Name must start with an uppercase letter'
25+
);
26+
await expect(await si.getDescription(page.getByLabel('Day of birth'))).toHaveText(
27+
'Day of birth required'
28+
);
29+
await expect(await si.getDescription(page.getByLabel('Fellow passengers'))).toHaveText(
30+
'Minimum 2'
31+
);
32+
await expect(
33+
await si.getDescription(page.getByLabel('I confirm that I accept all and everything'))
34+
).toHaveText('Accept terms before joining');
35+
36+
// fixing the name clears its errors
37+
await name.fill('Alice');
38+
await expect(await si.getDescription(name)).toHaveText('');
39+
});
40+
});
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
- heading "Signal form" [level=2]
2+
- text: Name*
3+
- textbox "Name*" [invalid]
4+
- text: Minimum 3 characters Name must start with an uppercase letter Description
5+
- textbox "Description"
6+
- text: Day of birth*
7+
- textbox "Day of birth*" [invalid]
8+
- text: Day of birth required Arrival
9+
- textbox "Arrival"
10+
- text: Departure
11+
- textbox "Departure"
12+
- text: Class of service
13+
- combobox "Class of service":
14+
- option "First class" [selected]
15+
- option "Business"
16+
- option "Economy"
17+
- text: Fellow passengers
18+
- spinbutton "Fellow passengers" [invalid]: "0"
19+
- text: Minimum 2
20+
- checkbox "I confirm that I accept all and everything*" [invalid]
21+
- text: I confirm that I accept all and everything* Accept terms before joining
22+
- checkbox "I confirm that I do not care about my privacy"
23+
- text: I confirm that I do not care about my privacy
24+
- button "Cancel"
25+
- button "Save"
26+
- heading "Debugging" [level=2]
27+
- paragraph: "Validation status: INVALID"
28+
- table:
29+
- rowgroup:
30+
- row "Model Submitted":
31+
- columnheader "Model"
32+
- columnheader "Submitted"
33+
- rowgroup:
34+
- 'row "{ \"name\": \"a\", \"description\": \"\", \"birthday\": \"\", \"arrival\": \"\", \"departure\": \"\", \"serviceClass\": \"first\", \"fellowPassengers\": 0, \"termsAccepted\": false, \"privacyDeclined\": false } undefined"':
35+
- 'cell "{ \"name\": \"a\", \"description\": \"\", \"birthday\": \"\", \"arrival\": \"\", \"departure\": \"\", \"serviceClass\": \"first\", \"fellowPassengers\": 0, \"termsAccepted\": false, \"privacyDeclined\": false }"'
36+
- cell "undefined"
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
- heading "Signal form" [level=2]
2+
- text: Name*
3+
- textbox "Name*"
4+
- text: Description
5+
- textbox "Description"
6+
- text: Day of birth*
7+
- textbox "Day of birth*"
8+
- text: Arrival
9+
- textbox "Arrival"
10+
- text: Departure
11+
- textbox "Departure"
12+
- text: Class of service
13+
- combobox "Class of service":
14+
- option "First class" [selected]
15+
- option "Business"
16+
- option "Economy"
17+
- text: Fellow passengers
18+
- spinbutton "Fellow passengers": "0"
19+
- checkbox "I confirm that I accept all and everything*"
20+
- text: I confirm that I accept all and everything*
21+
- checkbox "I confirm that I do not care about my privacy"
22+
- text: I confirm that I do not care about my privacy
23+
- button "Cancel"
24+
- button "Save"
25+
- heading "Debugging" [level=2]
26+
- paragraph: "Validation status: INVALID"
27+
- table:
28+
- rowgroup:
29+
- row "Model Submitted":
30+
- columnheader "Model"
31+
- columnheader "Submitted"
32+
- rowgroup:
33+
- 'row "{ \"name\": \"\", \"description\": \"\", \"birthday\": \"\", \"arrival\": \"\", \"departure\": \"\", \"serviceClass\": \"first\", \"fellowPassengers\": 0, \"termsAccepted\": false, \"privacyDeclined\": false } undefined"':
34+
- 'cell "{ \"name\": \"\", \"description\": \"\", \"birthday\": \"\", \"arrival\": \"\", \"departure\": \"\", \"serviceClass\": \"first\", \"fellowPassengers\": 0, \"termsAccepted\": false, \"privacyDeclined\": false }"'
35+
- cell "undefined"

projects/element-ng/form/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* SPDX-License-Identifier: MIT
44
*/
55
export * from './si-form-container/si-form-container.component';
6+
export * from './si-form-field/si-form-field.component';
7+
export * from './si-form-field/si-form-field.provider';
68
export * from './si-form-item/si-form-item.component';
79
export * from './si-form-validation-tooltip/si-form-validation-tooltip.directive';
810
export * from './form-fieldset/si-form-fieldset.component';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@if (!isFormCheck()) {
2+
<ng-container [ngTemplateOutlet]="labelTemplate" />
3+
}
4+
<div class="form-item-content">
5+
<ng-content />
6+
@if (isFormCheck()) {
7+
<ng-container [ngTemplateOutlet]="labelTemplate" />
8+
}
9+
<ng-container [ngTemplateOutlet]="feedbackTemplate" />
10+
</div>
11+
12+
<ng-template #labelTemplate>
13+
<label
14+
[for]="controlId()"
15+
[class.form-label]="!isFormCheck()"
16+
[class.form-check-label]="isFormCheck()"
17+
[class.required]="required()"
18+
>{{ label() | translate }}</label
19+
>
20+
</ng-template>
21+
22+
<ng-template #feedbackTemplate>
23+
<div class="invalid-feedback" [class.d-block]="errors().length" [attr.id]="errormessageId()">
24+
@for (error of errors(); track $index) {
25+
<div>
26+
{{ error.message | translate }}
27+
</div>
28+
}
29+
</div>
30+
</ng-template>

0 commit comments

Comments
 (0)