use signal forms

This commit is contained in:
toni
2026-01-03 01:17:40 +01:00
parent 00235a769a
commit 5a19b3a7a6
12 changed files with 298 additions and 296 deletions
@@ -1,13 +1,17 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<p>Name: {{ member.vorname }} {{ member.nachname }}</p>
<p>Passnummer: {{ member.passnummer }}</p>
<p>Geburtsdatum: {{ member.geburtsdatum??undefined | datePipe }}</p>
<mat-form-field>
<mat-label>K&uuml;ndigun zum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="kuendigungzum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" type="submit">Speichern</button>
<form>
<p>Name: {{ member.vorname }} {{ member.nachname }}</p>
<p>Passnummer: {{ member.passnummer }}</p>
<p>Geburtsdatum: {{ member.geburtsdatum | datePipe }}</p>
<mat-form-field>
<mat-label>K&uuml;ndigun zum</mat-label>
<input matInput [matDatepicker]="picker" [field]="form.kuendigungzum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
[for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="submit()">
Speichern
</button>
</form>
@@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Component, inject, signal } from '@angular/core';
import { Field, form } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
@@ -9,18 +9,17 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import * as api from '../../generated-api/api';
import { DatePipePipe, dateToLocal } from '../../pipes/date-pipe.pipe.js';
import {
DatePipePipe,
dateToLocal,
dateToUTC,
} from '../../pipes/date-pipe.pipe.js';
import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
type DeletionForm = {
kuendigungzum: FormControl<Date | null>;
};
@Component({
selector: 'app-delete-member-dialog',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
@@ -29,32 +28,31 @@ type DeletionForm = {
MatOptionModule,
MatDatepickerModule,
DatePipePipe,
Field,
],
templateUrl: './delete-member-dialog.component.html',
styleUrl: './delete-member-dialog.component.scss',
})
export class DeleteMemberDialogComponent {
form: FormGroup<DeletionForm>;
member: api.MemberDto;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto },
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
this.member = data.member;
this.form = new FormGroup<DeletionForm>({
kuendigungzum: new FormControl(null),
});
}
private readonly data = inject<{ member: api.MemberDto }>(MAT_DIALOG_DATA);
private readonly dialogRef = inject(
MatDialogRef<EditMemberDialogComponent>,
);
private readonly deletionModel = signal({
kuendigungzum: this.data.member.kuendigungzum
? dateToUTC(this.data.member.kuendigungzum)
: null,
});
protected readonly form = form(this.deletionModel);
protected readonly member = this.data.member;
async submit() {
const date = this.form.controls.kuendigungzum.value;
if (date) {
await api.scheduleDeletion(
this.member.id!,
dateToLocal(date).toISOString(),
);
protected async submit() {
const date = this.deletionModel().kuendigungzum;
await api.scheduleDeletion(
this.member.id,
date ? dateToLocal(date).toISOString() : '',
);
this.dialogRef.close();
}
this.dialogRef.close();
}
}
@@ -1,44 +1,41 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput placeholder="Vorname" formControlName="vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput placeholder="Nachname" formControlName="nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput placeholder="Geschlecht" formControlName="geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Passnummer</mat-label>
<input matInput placeholder="Passnummer" formControlName="passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput placeholder="Geburtsort" formControlName="geburtsort" />
</mat-form-field>
<mat-form-field>
<mat-label>Unterrichtsvertrag?</mat-label>
<mat-select matInput formControlName="uvertrag">
<mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Vereinsmitglied?</mat-label>
<mat-select matInput formControlName="verein">
<mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="submit">Speichern</button>
<form>
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput placeholder="Vorname" [field]="form.vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput placeholder="Nachname" [field]="form.nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput placeholder="Geschlecht" [field]="form.geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Passnummer</mat-label>
<input matInput placeholder="Passnummer" [field]="form.passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" [field]="form.geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
[for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput placeholder="Geburtsort" [field]="form.geburtsort" />
</mat-form-field>
<mat-form-field>
<mat-label>Vereinsmitglied?</mat-label>
<mat-select matInput [field]="form.verein">
<mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" (click)="submit()">
Speichern
</button>
</form>
@@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Component, inject, signal } from '@angular/core';
import { Field, form } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
@@ -9,13 +9,12 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import * as api from '../../generated-api/api';
import { MemberDtoHelper } from '../../util/member.js';
import { MemberDtoHelper, MemberEditForm } from '../../util/member.js';
@Component({
selector: 'app-edit-member-dialog',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
@@ -23,25 +22,28 @@ import { MemberDtoHelper } from '../../util/member.js';
MatSelectModule,
MatOptionModule,
MatDatepickerModule,
Field,
],
templateUrl: './edit-member-dialog.component.html',
styleUrl: './edit-member-dialog.component.scss',
})
export class EditMemberDialogComponent {
form: FormGroup;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto },
private memberDtoHelper: MemberDtoHelper,
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
this.form = this.memberDtoHelper.newMemberForm(data.member);
}
private readonly data = inject<{ member: api.MemberDto }>(MAT_DIALOG_DATA);
private readonly memberDtoHelper = inject(MemberDtoHelper);
private readonly dialogRef = inject(
MatDialogRef<EditMemberDialogComponent>,
);
submit() {
const member: api.MemberDto = this.memberDtoHelper.memberFromForm(
this.form,
private readonly memberModel = signal<MemberEditForm>(
this.memberDtoHelper.editMemberForm(this.data.member),
);
protected readonly form = form(this.memberModel);
protected async submit() {
const member: api.MemberDto = this.memberDtoHelper.memberFromSignal(
this.memberModel(),
);
api.updateMember(member);
await api.updateMember(member);
this.dialogRef.close();
}
}
@@ -1,23 +1,23 @@
<h1>Neues Mitglied anlegen</h1>
<form [formGroup]="memberForm" (ngSubmit)="onSubmit()">
<form>
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput formControlName="vorname" />
<input matInput [field]="memberForm.vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput formControlName="nachname" />
<input matInput [field]="memberForm.nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput formControlName="geschlecht" />
<input matInput [field]="memberForm.geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input
matInput
[matDatepicker]="picker"
formControlName="geburtsdatum" />
[field]="memberForm.geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
@@ -26,18 +26,18 @@
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput formControlName="geburtsort" />
<input matInput [field]="memberForm.geburtsort" />
</mat-form-field>
<mat-checkbox [formControl]="memberForm.controls.verein"
>Verein Beigetreten?</mat-checkbox
>
<mat-checkbox [field]="memberForm.verein">Verein Beigetreten?</mat-checkbox>
<mat-form-field>
<mat-label>Passnummer (falls vorhanden)</mat-label>
<input matInput formControlName="passnummer" />
<input matInput [field]="memberForm.passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Kontakt</mat-label>
<input matInput formControlName="kontakt" />
<input matInput [field]="memberForm.kontakt" />
</mat-form-field>
<button mat-flat-button color="primary" type="submit">Hinzufügen</button>
<button mat-flat-button color="primary" (click)="onSubmit()">
Hinzufügen
</button>
</form>
@@ -1,5 +1,5 @@
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Component, inject, signal } from '@angular/core';
import { Field, form } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -7,43 +7,49 @@ import { MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import * as api from '../../generated-api/api.js';
import { MemberDtoHelper } from '../../util/member.js';
import { MemberCreateForm, MemberDtoHelper } from '../../util/member.js';
@Component({
selector: 'app-new-member',
standalone: true,
providers: [],
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatDatepickerModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
Field,
],
templateUrl: './new-member.component.html',
styleUrl: './new-member.component.scss',
})
export class NewMemberComponent {
private readonly memberDtoHelper = inject(MemberDtoHelper);
constructor() {}
memberForm = this.memberDtoHelper.newMemberForm();
private dialogRef: MatDialogRef<NewMemberComponent> = inject(
private readonly dialogRef: MatDialogRef<NewMemberComponent> = inject(
MatDialogRef<NewMemberComponent>,
);
onSubmit() {
const newMember: api.MemberCreateArgs =
this.memberDtoHelper.newMemberFromForm(this.memberForm);
private readonly memberModel = signal<MemberCreateForm>(
this.memberDtoHelper.newMemberForm(),
);
protected readonly memberForm = form(this.memberModel);
api.createMember(newMember)
.then(() => {
console.log('Successfully added Member');
this.dialogRef.close();
})
.catch(e => {
console.error('Error adding member', e);
});
protected async onSubmit(): Promise<void> {
if (!this.memberForm().valid()) {
return;
}
const newMember = this.memberDtoHelper.newMemberFromSignal(
this.memberModel(),
);
try {
await api.createMember(newMember);
console.log('Successfully added Member');
this.dialogRef.close();
} catch (e) {
console.error('Error adding member', e);
}
}
}
@@ -9,10 +9,13 @@
<div class="filter-container">
<mat-form-field>
<label>Filter:</label>
<input matInput [formControl]="filterControl" placeholder="Filter" />
<input
matInput
[field]="filterForm.filterString"
placeholder="Filter" />
</mat-form-field>
<div class="status-filters">
<mat-radio-group [formControl]="statusControl">
<mat-radio-group [field]="filterForm.status">
<mat-radio-button [value]="MemberStatusFilter.ACTIVE"
>Aktiv</mat-radio-button
>
@@ -30,20 +33,21 @@
<input
matInput
[matDatepicker]="picker"
[formControl]="statusDateControl"
[field]="filterForm.statusDate"
placeholder="Aktiv bis" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle
matIconSuffix
[disabled]="statusControl.value===MemberStatusFilter.ALL"
[disabled]="statusDateDisabled()"
[for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<mat-checkbox [formControl]="onlyWithID">Nur mit Passnummer</mat-checkbox>
<mat-checkbox [field]="filterForm.onlyWithID"
>Nur mit Passnummer</mat-checkbox
>
</div>
<div class="table-container">
<!--TODO implement row count, selecting and alternating colors-->
<table
mat-table
[dataSource]="dataSource"
@@ -1,6 +1,14 @@
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
computed,
effect,
inject,
signal,
viewChild,
} from '@angular/core';
import { Field, form } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -13,7 +21,6 @@ import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { combineLatest, startWith } from 'rxjs';
import * as api from '../../generated-api/api';
import { DatePipePipe } from '../../pipes/date-pipe.pipe.js';
import { NobreakHyphenPipe } from '../../pipes/nobreak-hyphen.pipe.js';
@@ -40,25 +47,34 @@ enum MemberStatusFilter {
RouterModule,
MatButtonModule,
DatePipePipe,
ReactiveFormsModule,
MatDatepickerModule,
MatTooltipModule,
MatCheckbox,
MatRadioModule,
MatSortModule,
Field,
],
templateUrl: './view-members.html',
styleUrl: './view-members.scss',
})
export class ViewMembers implements AfterViewInit {
protected MemberStatusFilter = MemberStatusFilter;
protected readonly filterControl = new FormControl<string>('');
protected readonly statusControl = new FormControl<MemberStatusFilter>(
MemberStatusFilter.ACTIVE,
private readonly changeDetectorRef = inject(ChangeDetectorRef);
protected readonly MemberStatusFilter = MemberStatusFilter;
private readonly filterModel = signal({
filterString: '',
status: MemberStatusFilter.ACTIVE,
statusDate: null as Date | null,
onlyWithID: false,
});
protected readonly filterForm = form(this.filterModel);
protected readonly statusDateDisabled = computed(
() => this.filterForm.status().value() === MemberStatusFilter.ALL,
);
protected readonly statusDateControl = new FormControl<Date | null>(null);
protected readonly onlyWithID = new FormControl<boolean>(false);
protected dataSource = new MatTableDataSource<api.MemberDto>();
protected readonly dataSource = new MatTableDataSource<api.MemberDto>();
private readonly datePipe = new DatePipePipe();
protected readonly displayedColumns: string[] = [
@@ -159,7 +175,7 @@ export class ViewMembers implements AfterViewInit {
return originalFilterPredicate(
{
...data,
id: null,
id: '',
geburtsdatum: this.datePipe.transform(
data.geburtsdatum ?? undefined,
),
@@ -173,42 +189,11 @@ export class ViewMembers implements AfterViewInit {
filter,
);
};
combineLatest([
this.filterControl.valueChanges.pipe(
startWith(this.filterControl.value),
),
this.statusControl.valueChanges.pipe(
startWith(this.statusControl.value),
),
this.statusDateControl.valueChanges.pipe(
startWith(this.statusDateControl.value),
),
this.onlyWithID.valueChanges.pipe(startWith(this.onlyWithID.value)),
])
// .pipe(takeUntilDestroyed())
.subscribe(combined => {
const filter = combined[0];
const status = combined[1];
const date = combined[2];
const onlyWithID = combined[3];
this.dataSource.filter = `${filter?.replaceAll(';', '') ?? ''};${status};${date?.toISOString()};${onlyWithID}`;
});
this.statusControl.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(value => {
if (
value === MemberStatusFilter.ALL &&
this.statusDateControl.enabled
) {
return this.statusDateControl.disable();
} else if (
value !== MemberStatusFilter.ALL &&
this.statusDateControl.disabled
) {
return this.statusDateControl.enable();
}
});
effect(() => {
const filters = this.filterModel();
this.dataSource.filter = `${filters.filterString?.replaceAll(';', '') ?? ''};${filters.status};${filters.statusDate?.toISOString()};${filters.onlyWithID}`;
});
this.updateData();
}
@@ -221,5 +206,6 @@ export class ViewMembers implements AfterViewInit {
private async updateData(): Promise<void> {
const members = await api.getMembers();
this.dataSource.data = members;
this.changeDetectorRef.detectChanges();
}
}