Skip to content

Commit f8c2122

Browse files
committed
refactor: switch to component usage
1 parent 0b2d13a commit f8c2122

File tree

7 files changed

+93
-16
lines changed

7 files changed

+93
-16
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@for (message of messages(); track $index) {
2+
<small [class]="message.cssClass">{{ message.info }}.</small>
3+
@if(!$last) {
4+
<br />
5+
} }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
small {
2+
position: relative;
3+
top: -1rem;
4+
5+
&.pending {
6+
color: #886b02;
7+
}
8+
9+
&.invalid {
10+
color: var(--pico-del-color);
11+
}
12+
13+
&.valid {
14+
color: var(--pico-ins-color);
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { FormFieldInfo } from './form-field-info';
4+
5+
describe('FormFieldInfo', () => {
6+
let component: FormFieldInfo;
7+
let fixture: ComponentFixture<FormFieldInfo>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [FormFieldInfo]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(FormFieldInfo);
16+
component = fixture.componentInstance;
17+
await fixture.whenStable();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Component, computed, input, signal } from '@angular/core';
2+
import { FieldTree } from '@angular/forms/signals';
3+
4+
import { FIELD_INFO } from '../form-props';
5+
6+
@Component({
7+
selector: 'app-form-field-info',
8+
imports: [],
9+
templateUrl: './form-field-info.html',
10+
styleUrl: './form-field-info.scss',
11+
})
12+
export class FormFieldInfo<T> {
13+
readonly fieldRef = input.required<FieldTree<T>>();
14+
15+
protected readonly messages = computed(() => {
16+
const field = this.fieldRef()();
17+
let messages: { info: string; cssClass: 'info' | 'pending' | 'valid' | 'invalid' }[] = [];
18+
19+
if (field.pending()) {
20+
messages = [{ info: 'Checking availability ...', cssClass: 'pending' }];
21+
} else if (field.touched() && field.errors().length > 0) {
22+
messages = field.errors().map((e) => ({ info: e.message || 'Invalid', cssClass: 'invalid' }));
23+
} else if (field.hasMetadata(FIELD_INFO)) {
24+
messages = [{ info: field.metadata(FIELD_INFO)!, cssClass: field.valid() ? 'valid': 'info' }];
25+
}
26+
return messages;
27+
});
28+
}

src/app/form-props.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createMetadataKey } from "@angular/forms/signals";
2+
3+
export const FIELD_INFO = createMetadataKey<string>()

src/app/registration-form-3/registration-form-3.html

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
1111
>Username
1212
<input
1313
type="text"
14+
id="myField"
1415
[field]="registrationForm.username"
1516
[aria-invalid]="ariaInvalidState(registrationForm.username)"
1617
/>
17-
@if (registrationForm.username().pending()) {
18-
<small>Checking availability ...</small>
19-
}
20-
<app-form-error [fieldRef]="registrationForm.username" />
18+
<app-form-field-info [fieldRef]="registrationForm.username" />
2119
</label>
2220

2321
<!-- A whole child form with own model -->
@@ -32,7 +30,7 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
3230
[field]="registrationForm.age"
3331
[aria-invalid]="ariaInvalidState(registrationForm.age)"
3432
/>
35-
<app-form-error [fieldRef]="registrationForm.age" />
33+
<app-form-field-info [fieldRef]="registrationForm.age" />
3634
</label>
3735
</div>
3836

@@ -45,7 +43,7 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
4543
[field]="registrationForm.password.pw1"
4644
[aria-invalid]="ariaInvalidState(registrationForm.password.pw1)"
4745
/>
48-
<app-form-error [fieldRef]="registrationForm.password.pw1" />
46+
<app-form-field-info [fieldRef]="registrationForm.password.pw1" />
4947
</label>
5048
<label
5149
>Password Confirmation
@@ -55,9 +53,9 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
5553
[field]="registrationForm.password.pw2"
5654
[aria-invalid]="ariaInvalidState(registrationForm.password.pw2)"
5755
/>
58-
<app-form-error [fieldRef]="registrationForm.password.pw2" />
56+
<app-form-field-info [fieldRef]="registrationForm.password.pw2" />
5957
</label>
60-
<app-form-error [fieldRef]="registrationForm.password" />
58+
<app-form-field-info [fieldRef]="registrationForm.password" />
6159
</div>
6260
<fieldset>
6361
<legend>
@@ -76,11 +74,11 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
7674
/>
7775
<button type="button" (click)="removeEmail($index)">-</button>
7876
</div>
79-
<app-form-error [fieldRef]="emailField" />
77+
<app-form-field-info [fieldRef]="emailField" />
8078
</div>
8179
}
8280
</div>
83-
<app-form-error [fieldRef]="registrationForm.email" />
81+
<app-form-field-info [fieldRef]="registrationForm.email" />
8482
</fieldset>
8583
<label
8684
>Subscribe to Newsletter?
@@ -91,7 +89,7 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
9189
[selectOptions]="['Angular', 'React', 'Vue', 'Svelte']"
9290
label="Topics (multiple possible):"
9391
/>
94-
<app-form-error [fieldRef]="registrationForm.newsletterTopics" />
92+
<app-form-field-info [fieldRef]="registrationForm.newsletterTopics" />
9593
<label
9694
>I agree to the terms and conditions
9795
<input
@@ -100,9 +98,9 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
10098
[field]="registrationForm.agreeToTermsAndConditions"
10199
/>
102100
</label>
103-
<app-form-error [fieldRef]="registrationForm.agreeToTermsAndConditions" />
101+
<app-form-field-info [fieldRef]="registrationForm.agreeToTermsAndConditions" />
104102
<hr />
105-
<app-form-error [fieldRef]="registrationForm" />
103+
<app-form-field-info [fieldRef]="registrationForm" />
106104
<div role="group">
107105
<button
108106
type="submit"

src/app/registration-form-3/registration-form-3.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Component, inject, resource, signal } from '@angular/core';
2-
import { apply, applyEach, applyWhen, debounce, disabled, email, Field, FieldTree, form, maxLength, min, minLength, pattern, required, schema, submit, validate, validateAsync, validateTree, ValidationError, WithField } from '@angular/forms/signals';
2+
import { apply, applyEach, applyWhen, debounce, disabled, email, Field, FieldTree, form, maxLength, metadata, min, minLength, pattern, required, schema, submit, validate, validateAsync, validateTree, ValidationError, WithField } from '@angular/forms/signals';
33

44
import { BackButton } from '../back-button/back-button';
55
import { DebugOutput } from '../debug-output/debug-output';
6-
import { FormError } from '../form-error/form-error';
6+
import { FormFieldInfo } from '../form-field-info/form-field-info';
7+
import { FIELD_INFO } from '../form-props';
78
import { GenderIdentity, IdentityForm, identitySchema, initialGenderIdentityState } from '../identity-form/identity-form';
89
import { Multiselect } from '../multiselect/multiselect';
910
import { RegistrationService } from '../registration-service';
@@ -60,6 +61,7 @@ export const formSchema = schema<RegisterFormData>((schemaPath) => {
6061
},
6162
onError: () => undefined
6263
});
64+
metadata(schemaPath.username, FIELD_INFO, () => "A username must consists of 3-12 characters.")
6365

6466
// Age validation
6567
min(schemaPath.age, 18, { message: 'You must be >=18 years old.' });
@@ -81,6 +83,7 @@ export const formSchema = schema<RegisterFormData>((schemaPath) => {
8183
applyEach(schemaPath.email, (emailPath) => {
8284
email(emailPath, { message: 'E-Mail format is invalid' });
8385
});
86+
metadata(schemaPath.email, FIELD_INFO, () => "Please enter at least one valid E-Mail address")
8487

8588
// Password validation
8689
required(schemaPath.password.pw1, { message: 'A password is required' });
@@ -104,6 +107,7 @@ export const formSchema = schema<RegisterFormData>((schemaPath) => {
104107
message: 'The entered password must match with the one specified in "Password" field',
105108
};
106109
});
110+
metadata(schemaPath.password, FIELD_INFO, () => "Please enter a password with min 8 characters and a special character.")
107111

108112
// Newsletter validation
109113
applyWhen(
@@ -130,7 +134,7 @@ export const formSchema = schema<RegisterFormData>((schemaPath) => {
130134

131135
@Component({
132136
selector: 'app-registration-form-3',
133-
imports: [BackButton, Field, DebugOutput, FormError, IdentityForm, Multiselect],
137+
imports: [BackButton, Field, DebugOutput, FormFieldInfo, IdentityForm, Multiselect],
134138
templateUrl: './registration-form-3.html',
135139
styleUrl: './registration-form-3.scss',
136140
// Also possible: set SignalFormsConfig only for local component:

0 commit comments

Comments
 (0)