Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
799 changes: 404 additions & 395 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@
},
"private": true,
"dependencies": {
"@angular/common": "^21.0.0-next.8",
"@angular/compiler": "^21.0.0-next.8",
"@angular/core": "^21.0.0-next.8",
"@angular/forms": "^21.0.0-next.8",
"@angular/platform-browser": "^21.0.0-next.8",
"@angular/router": "^21.0.0-next.8",
"@angular/common": "^21.0.0-next.9",
"@angular/compiler": "^21.0.0-next.9",
"@angular/core": "^21.0.0-next.9",
"@angular/forms": "^21.0.0-next.9",
"@angular/platform-browser": "^21.0.0-next.9",
"@angular/router": "^21.0.0-next.9",
"@picocss/pico": "^2.1.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^21.0.0-next.8",
"@angular/cli": "^21.0.0-next.8",
"@angular/compiler-cli": "^21.0.0-next.8",
"@angular/build": "^21.0.0-next.9",
"@angular/cli": "^21.0.0-next.9",
"@angular/compiler-cli": "^21.0.0-next.9",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.10.0",
"karma": "~6.4.0",
Expand Down
15 changes: 15 additions & 0 deletions src/app/field-info/field-info.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@if (isFieldRequired()) {
<b>*</b>
} @if(hasSpecificInfo()) {
<div>
@if (fieldMin()) {
<small>(Minimum {{ fieldMin() }})</small>
} @if (fieldOneInList()) {
<small>(Minimum one, multiple possible)</small>
} @if (fieldRange()) {
<small>({{ fieldRange() }} characters)</small>
} @if (isValidationPending()) {
<small><i>Checking availability ...</i></small>
}
</div>
}
1 change: 1 addition & 0 deletions src/app/field-info/field-info.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

23 changes: 23 additions & 0 deletions src/app/field-info/field-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { FieldInfo } from './field-info';

describe('FieldInfo', () => {
let component: FieldInfo;
let fixture: ComponentFixture<FieldInfo>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FieldInfo]
})
.compileComponents();

fixture = TestBed.createComponent(FieldInfo);
component = fixture.componentInstance;
await fixture.whenStable();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
21 changes: 21 additions & 0 deletions src/app/field-info/field-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component, computed, input } from '@angular/core';
import { FieldTree, MAX_LENGTH, MIN, MIN_LENGTH, REQUIRED } from '@angular/forms/signals';
import { MIN_ONE_IN_LIST } from '../form-props';

@Component({
selector: 'app-field-info',
imports: [],
templateUrl: './field-info.html',
styleUrl: './field-info.scss'
})
export class FieldInfo<T> {
readonly fieldRef = input.required<FieldTree<T>>();
protected readonly isFieldRequired = computed(() => this.fieldRef()().property(REQUIRED)());
private readonly fieldMinLength = computed(() => this.fieldRef()().property(MIN_LENGTH)());
private readonly fieldMaxLength = computed(() => this.fieldRef()().property(MAX_LENGTH)());
protected readonly fieldRange = computed(() => !!this.fieldMinLength() && !!this.fieldMaxLength() && `${this.fieldMinLength()}..${this.fieldMaxLength()}`);
protected readonly fieldMin = computed(() => this.fieldRef()().property(MIN)());
protected readonly isValidationPending = computed(() => this.fieldRef()().pending());
protected readonly fieldOneInList = computed(() => this.fieldRef()().property(MIN_ONE_IN_LIST));
protected readonly hasSpecificInfo = computed(() => this.fieldMin() || this.fieldOneInList() || this.fieldRange() || this.isValidationPending());
}
3 changes: 3 additions & 0 deletions src/app/form-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createProperty } from "@angular/forms/signals";

export const MIN_ONE_IN_LIST = createProperty<boolean>()
12 changes: 6 additions & 6 deletions src/app/identity-form/identity-form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<label
>Gender
<label>
Gender <app-field-info [fieldRef]="identity().gender" />
<select
name="gender-identity"
[field]="identity().gender"
Expand All @@ -14,14 +14,14 @@

<div class="group-with-gap">
@if (!identity().salutation().hidden()) {
<label
>Salutation
<label>
Salutation <app-field-info [fieldRef]="identity().salutation" />
<input type="text" placeholder="e. g. Mx." [field]="identity().salutation" />
<app-form-error [fieldRef]="identity().salutation" />
</label>
} @if (!identity().pronoun().hidden()) {
<label
>Pronoun
<label>
Pronoun <app-field-info [fieldRef]="identity().pronoun" />
<input type="text" placeholder="e. g. they/them" [field]="identity().pronoun" />
<app-form-error [fieldRef]="identity().pronoun" />
</label>
Expand Down
6 changes: 5 additions & 1 deletion src/app/identity-form/identity-form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, model } from '@angular/core';
import { Field, FieldTree, hidden, required, schema } from '@angular/forms/signals';
import { FormError } from '../form-error/form-error';
import { FieldInfo } from '../field-info/field-info';

export interface GenderIdentity {
gender: '' | 'male' | 'female' | 'diverse';
Expand All @@ -22,6 +23,9 @@ export const identitySchema = schema<GenderIdentity>((path) => {
return !ctx.valueOf(path.gender) || ctx.valueOf(path.gender) !== 'diverse';
});

required(path.gender, {
message: 'Please select a gender',
});
required(path.salutation, {
when: (ctx) => ctx.valueOf(path.gender) === 'diverse',
message: 'Please choose a salutation, when diverse gender selected',
Expand All @@ -34,7 +38,7 @@ export const identitySchema = schema<GenderIdentity>((path) => {

@Component({
selector: 'app-identity-form',
imports: [Field, FormError],
imports: [Field, FormError, FieldInfo],
templateUrl: './identity-form.html',
styleUrl: './identity-form.scss',
})
Expand Down
37 changes: 16 additions & 21 deletions src/app/registration-form-3/registration-form-3.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
</p>

<form (submit)="submitForm($event)" novalidate>
<label
>Username
<label>
Username <app-field-info [fieldRef]="registrationForm.username" />
<input
type="text"
[field]="registrationForm.username"
[ariaInvalid]="ariaInvalidState(registrationForm.username)"
/>
@if (registrationForm.username().pending()) {
<small>Checking availability ...</small>
}
<app-form-error [fieldRef]="registrationForm.username" />
</label>

Expand All @@ -25,8 +22,8 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>

<!-- native HTML inputs bound with the [field] directive -->
<div>
<label
>Age
<label>
Age <app-field-info [fieldRef]="registrationForm.age" />
<input
type="number"
[field]="registrationForm.age"
Expand All @@ -37,8 +34,8 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
</div>

<div>
<label
>Password
<label>
Password <app-field-info [fieldRef]="registrationForm.password.pw1" />
<input
type="password"
autocomplete
Expand All @@ -47,8 +44,8 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
/>
<app-form-error [fieldRef]="registrationForm.password.pw1" />
</label>
<label
>Password Confirmation
<label>
Password Confirmation <app-field-info [fieldRef]="registrationForm.password.pw2" />
<input
type="password"
autocomplete
Expand All @@ -63,6 +60,7 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
<legend>
E-Mail Addresses
<button type="button" (click)="addEmail()">+</button>
<app-field-info [fieldRef]="registrationForm.email" />
</legend>
<div>
@for (emailField of registrationForm.email; track $index) {
Expand All @@ -82,17 +80,18 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
</div>
<app-form-error [fieldRef]="registrationForm.email" />
</fieldset>
<label
>Subscribe to Newsletter?
<label>
Subscribe to Newsletter? <app-field-info [fieldRef]="registrationForm.newsletter" />
<input type="checkbox" [field]="registrationForm.newsletter" />
</label>
<app-multiselect
[field]="registrationForm.newsletterTopics"
label="Topics (multiple possible):"
/>
<app-form-error [fieldRef]="registrationForm.newsletterTopics" />
<label
>I agree to the terms and conditions
<label>
I agree to the terms and conditions
<app-field-info [fieldRef]="registrationForm.agreeToTermsAndConditions" />
<input
type="checkbox"
[ariaInvalid]="ariaInvalidState(registrationForm.agreeToTermsAndConditions)"
Expand All @@ -108,14 +107,10 @@ <h1>Version 3: Child Forms and Custom UI Controls</h1>
[disabled]="registrationForm().submitting()"
[ariaBusy]="registrationForm().submitting()"
>
@if (registrationForm().submitting()) {
Registering ...
} @else {
Register
}
@if (registrationForm().submitting()) { Registering ... } @else { Register }
</button>
<button type="reset" (click)="resetForm()">Reset</button>
</div>
</form>

<app-debug-output [form]="registrationForm"/>
<app-debug-output [form]="registrationForm" />
7 changes: 7 additions & 0 deletions src/app/registration-form-3/registration-form-3.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
legend {
width: 100%;

> button {
float: right;
}
}
9 changes: 7 additions & 2 deletions src/app/registration-form-3/registration-form-3.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Component, inject, resource, signal } from '@angular/core';
import { apply, applyEach, applyWhen, Field, customError, CustomValidationError, disabled, email, FieldTree, form, maxLength, min, minLength, pattern, required, schema, submit, validate, validateAsync, validateTree, ValidationError, WithField } from '@angular/forms/signals';
import { apply, applyEach, applyWhen, createProperty, customError, CustomValidationError, disabled, email, Field, FieldTree, form, maxLength, min, minLength, orProperty, pattern, property, required, schema, submit, validate, validateAsync, validateTree, ValidationError, WithField } from '@angular/forms/signals';

import { BackButton } from '../back-button/back-button';
import { DebugOutput } from '../debug-output/debug-output';
import { FieldInfo } from '../field-info/field-info';
import { FormError } from '../form-error/form-error';
import { GenderIdentity, IdentityForm, identitySchema, initialGenderIdentityState } from '../identity-form/identity-form';
import { Multiselect } from '../multiselect/multiselect';
import { RegistrationService } from '../registration-service';
import { MIN_ONE_IN_LIST } from '../form-props';

export interface RegisterFormData {
username: string;
Expand Down Expand Up @@ -60,6 +62,7 @@ export const formSchema = schema<RegisterFormData>((fieldPath) => {
});

// Age validation
required(fieldPath.age, { message: 'You must enter an age.' });
min(fieldPath.age, 18, { message: 'You must be >=18 years old.' });

// Terms and conditions
Expand All @@ -79,6 +82,7 @@ export const formSchema = schema<RegisterFormData>((fieldPath) => {
applyEach(fieldPath.email, (emailPath) => {
email(emailPath, { message: 'E-Mail format is invalid' });
});
property(fieldPath.email, MIN_ONE_IN_LIST, () => true);

// Password validation
required(fieldPath.password.pw1, { message: 'A password is required' });
Expand Down Expand Up @@ -121,14 +125,15 @@ export const formSchema = schema<RegisterFormData>((fieldPath) => {

// Disable newsletter topics when newsletter is unchecked
disabled(fieldPath.newsletterTopics, (ctx) => !ctx.valueOf(fieldPath.newsletter));
property(fieldPath.newsletterTopics, MIN_ONE_IN_LIST, () => true);

// apply child schema for identity checks
apply(fieldPath.identity, identitySchema);
});

@Component({
selector: 'app-registration-form-3',
imports: [BackButton, Field, DebugOutput, FormError, IdentityForm, Multiselect],
imports: [BackButton, Field, DebugOutput, FormError, IdentityForm, Multiselect, FieldInfo],
templateUrl: './registration-form-3.html',
styleUrl: './registration-form-3.scss',
})
Expand Down