Kurs Angular · Moduł 8: Formularze
Typed Forms (Angular 14+)
W tej lekcji2
Typed Forms zapewniają pełne bezpieczeństwo typów - jak precyzyjne pociągnięcia mistrza kaligrafii!
Definiowanie Typów
import { FormControl, FormGroup, NonNullableFormBuilder } from '@angular/forms';
// Interfejs dla formularza
interface SamuraiForm {
name: FormControl<string>;
level: FormControl<number>;
clan: FormControl<string>;
skills: FormControl<string[]>;
}
@Component({...})
export class TypedFormComponent {
// NonNullableFormBuilder - kontrolki nie mogą być null
private fb = inject(NonNullableFormBuilder);
samuraiForm: FormGroup<SamuraiForm> = this.fb.group({
name: ['', Validators.required],
level: [1, Validators.min(1)],
clan: ['Ronin'],
skills: [['Kendo']]
});
onSubmit(): void {
// value jest w pełni typowane!
const formValue = this.samuraiForm.getRawValue();
// Typ: { name: string, level: number, clan: string, skills: string[] }
console.log(formValue.name); // string, nie string | null
}
}FormControl vs FormGroup vs FormArray
import { FormControl, FormGroup, FormArray } from '@angular/forms';
// FormControl - pojedyncza wartość
const nameControl = new FormControl<string>('');
// FormGroup - obiekt z polami
const samuraiGroup = new FormGroup({
name: new FormControl(''),
level: new FormControl(1)
});
// FormArray - tablica kontrolek
const skillsArray = new FormArray([
new FormControl('Kendo'),
new FormControl('Iaido')
]);
// Dostęp do wartości
console.log(nameControl.value); // string
console.log(samuraiGroup.value); // { name: string, level: number }
console.log(skillsArray.value); // string[]Kod do tej lekcji: src/app/app.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
import { Observable, of, delay, map } from 'rxjs';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<div class="registration-dojo">
<h1>Rejestracja w Akademii Samurajów</h1>
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<div class="form-field">
<label>Nazwa wojownika *</label>
<input formControlName="username" placeholder="Unikalna nazwa...">
@if (registrationForm.get('username')?.pending) {
<span class="checking">Sprawdzanie dostępności...</span>
}
@if (registrationForm.get('username')?.touched && registrationForm.get('username')?.errors) {
<div class="error-box">
@if (registrationForm.get('username')?.errors?.['required']) {
<span>Nazwa jest wymagana</span>
}
@if (registrationForm.get('username')?.errors?.['minlength']) {
<span>Minimum 4 znaki</span>
}
@if (registrationForm.get('username')?.errors?.['usernameTaken']) {
<span>Ta nazwa jest już zajęta!</span>
}
</div>
}
@if (registrationForm.get('username')?.valid) {
<span class="valid-mark">Nazwa dostępna</span>
}
</div>
<div class="form-field">
<label>Email *</label>
<input type="email" formControlName="email" placeholder="twoj@email.com">
@if (registrationForm.get('email')?.touched && registrationForm.get('email')?.errors) {
<div class="error-box">
@if (registrationForm.get('email')?.errors?.['required']) {
<span>Email jest wymagany</span>
}
@if (registrationForm.get('email')?.errors?.['email']) {
<span>Nieprawidłowy format email</span>
}
</div>
}
</div>
<div class="form-field" formGroupName="passwords">
<label>Hasło *</label>
<input type="password" formControlName="password" placeholder="Silne hasło...">
@if (registrationForm.get('passwords.password')?.touched) {
<div class="password-strength">
<div class="strength-bar" [style.width.%]="passwordStrength()">
</div>
<span>Siła: {{ passwordStrengthLabel() }}</span>
</div>
}
<label>Powtórz hasło *</label>
<input type="password" formControlName="confirmPassword" placeholder="Powtórz hasło...">
@if (registrationForm.get('passwords')?.errors?.['passwordMismatch'] &&
registrationForm.get('passwords.confirmPassword')?.touched) {
<div class="error-box">
<span>Hasła nie są identyczne!</span>
</div>
}
</div>
<div class="form-field">
<label class="checkbox-label">
<input type="checkbox" formControlName="acceptTerms">
Akceptuję kodeks Bushido (regulamin) *
</label>
@if (registrationForm.get('acceptTerms')?.touched &&
registrationForm.get('acceptTerms')?.errors?.['requiredTrue']) {
<div class="error-box">
<span>Musisz zaakceptować regulamin</span>
</div>
}
</div>
<div class="form-summary">
<h3>Status formularza:</h3>
<ul>
<li [class.valid]="registrationForm.get('username')?.valid">
Nazwa: {{ registrationForm.get('username')?.valid ? 'OK' : 'X' }}
</li>
<li [class.valid]="registrationForm.get('email')?.valid">
Email: {{ registrationForm.get('email')?.valid ? 'OK' : 'X' }}
</li>
<li [class.valid]="registrationForm.get('passwords')?.valid">
Hasła: {{ registrationForm.get('passwords')?.valid ? 'OK' : 'X' }}
</li>
<li [class.valid]="registrationForm.get('acceptTerms')?.valid">
Regulamin: {{ registrationForm.get('acceptTerms')?.valid ? 'OK' : 'X' }}
</li>
</ul>
</div>
<button type="submit" [disabled]="registrationForm.invalid || registrationForm.pending">
{{ registrationForm.pending ? '⏳ Sprawdzanie...' : 'Dołącz do Akademii' }}
</button>
</form>
@if (registered()) {
<div class="success-message">
<h2>Witaj w Akademii, {{ registrationForm.get('username')?.value }}!</h2>
<p>Twoja samurajska przygoda właśnie się zaczyna!</p>
</div>
}
</div>
`,
styles: [`
.registration-dojo {
font-family: 'Segoe UI', sans-serif;
padding: 20px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #eee;
}
h1 {
text-align: center;
color: #e94560;
}
form {
max-width: 500px;
margin: 0 auto;
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 15px;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
margin-bottom: 8px;
color: #e94560;
font-weight: bold;
}
.form-field input[type="text"],
.form-field input[type="email"],
.form-field input[type="password"] {
width: 100%;
padding: 12px;
border: 2px solid #0f3460;
border-radius: 8px;
background: #0f3460;
color: white;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 5px;
}
.form-field input:focus {
border-color: #e94560;
outline: none;
}
.form-field input.ng-valid.ng-touched {
border-color: #28a745;
}
.form-field input.ng-invalid.ng-touched {
border-color: #dc3545;
}
.checking {
color: #ffc107;
font-size: 0.9em;
}
.valid-mark {
color: #28a745;
font-size: 0.9em;
}
.error-box {
background: rgba(220, 53, 69, 0.2);
border: 1px solid #dc3545;
border-radius: 5px;
padding: 8px;
margin-top: 5px;
}
.error-box span {
display: block;
color: #fca5a5;
font-size: 0.85em;
}
.password-strength {
margin: 10px 0;
}
.strength-bar {
height: 8px;
background: linear-gradient(90deg, #dc3545 0%, #ffc107 50%, #28a745 100%);
border-radius: 4px;
transition: width 0.3s;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.checkbox-label input {
width: 20px;
height: 20px;
}
.form-summary {
background: rgba(0,0,0,0.3);
padding: 15px;
border-radius: 8px;
margin: 20px 0;
}
.form-summary h3 {
margin-top: 0;
color: #e94560;
}
.form-summary ul {
list-style: none;
padding: 0;
}
.form-summary li {
padding: 5px 0;
color: #888;
}
.form-summary li.valid {
color: #28a745;
}
button[type="submit"] {
width: 100%;
padding: 15px;
background: #e94560;
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: background 0.3s;
}
button[type="submit"]:hover:not(:disabled) {
background: #ff6b6b;
}
button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.success-message {
text-align: center;
background: rgba(40, 167, 69, 0.2);
border: 2px solid #28a745;
border-radius: 15px;
padding: 30px;
margin-top: 20px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.success-message h2 {
color: #28a745;
}
`]
})
export class AppComponent {
registrationForm: FormGroup;
registered = signal(false);
// Symulacja zajętych nazw
private takenUsernames = ['musashi', 'kojiro', 'yagyu', 'admin', 'samurai'];
constructor(private fb: FormBuilder) {
this.registrationForm = this.fb.group({
username: ['',
[Validators.required, Validators.minLength(4)],
[this.usernameAvailableValidator()]
],
email: ['', [Validators.required, Validators.email]],
passwords: this.fb.group({
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: this.passwordMatchValidator }),
acceptTerms: [false, Validators.requiredTrue]
});
}
// Async validator - sprawdza, czy nazwa jest dostępna
usernameAvailableValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
// Symulacja zapytania do API
return of(control.value).pipe(
delay(1000),
map(username => {
const isTaken = this.takenUsernames.includes(username.toLowerCase());
return isTaken ? { usernameTaken: true } : null;
})
);
};
}
// Cross-field validator
passwordMatchValidator(control: AbstractControl): ValidationErrors | null {
const password = control.get('password')?.value;
const confirmPassword = control.get('confirmPassword')?.value;
if (password && confirmPassword && password !== confirmPassword) {
return { passwordMismatch: true };
}
return null;
}
passwordStrength(): number {
const password = this.registrationForm.get('passwords.password')?.value || '';
let strength = 0;
if (password.length >= 8) strength += 25;
if (password.length >= 12) strength += 15;
if (/[A-Z]/.test(password)) strength += 20;
if (/[a-z]/.test(password)) strength += 10;
if (/[0-9]/.test(password)) strength += 15;
if (/[^A-Za-z0-9]/.test(password)) strength += 15;
return Math.min(strength, 100);
}
passwordStrengthLabel(): string {
const strength = this.passwordStrength();
if (strength < 30) return 'Słabe';
if (strength < 60) return 'Średnie';
if (strength < 80) return 'Dobre';
return 'Bardzo silne';
}
onSubmit(): void {
if (this.registrationForm.valid) {
this.registered.set(true);
}
}
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zapewniają Typed Forms (Angular 14+)?
2. Co gwarantuje użycie NonNullableFormBuilder?
To 2 z 6 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj bloki budujące formularz od najmniejszego do największego zakresu (od pojedynczej wartości do tablicy grup).
- Edytor kodu
Uzupełnij szablon wyświetlający błędy walidacji tylko gdy pole było dotknięte.
- Klikanie w kolejności
Zbuduj definicję kontrolki z walidatorami: ['', [Validators.required, Validators.email]]. Klikaj elementy po kolei.