Изучение форм Angular: Новая альтернатива с помощью Signals
В мире Angular формы необходимы для взаимодействия с пользователем, независимо от того, создаете ли вы простую страницу входа в систему или более сложный интерфейс профиля пользователя. Angular традиционно предлагает два основных подхода: формы, управляемые шаблонами, и реактивные формы.
Новый инструмент для управления реактивностью — signals — был представлен в версии 16 Angular и с тех пор находится в центре внимания разработчиков Angular, став стабильным с версией 17. Сигналы позволяют вам декларативно обрабатывать изменения состояния, предлагая интересную альтернативу, которая сочетает в себе простоту форм, управляемых шаблонами, и высокую реактивность реактивных форм. В этой статье мы рассмотрим, как сигналы могут повысить реактивность как простых, так и сложных форм в Angular.
Краткое описание: Подходы к использованию Angular Forms
Прежде чем углубиться в тему улучшения форм, управляемых шаблонами, с помощью signals, давайте кратко рассмотрим традиционные подходы Angular к формам:
- Формы, управляемые шаблонами: эти формы, определенные непосредственно в HTML-шаблоне с помощью директив, таких как
ngModel
, просты в настройке и идеально подходят для простых форм. Однако они могут не обеспечивать детальный контроль, необходимый для более сложных сценариев.
Вот минимальный пример формы, управляемой шаблоном:
<form (ngSubmit)="onSubmit()">
<label for="name">Name:</label>
<input id="name" [(ngModel)]="name" name="name">
<button type="submit">Submit</button>
</form>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
name = '';
onSubmit() {
console.log(this.name);
}
}
- Реактивные формы: программно управляются в классе component с помощью классов Angular
FormGroup
,FormControl
иFormArray
; реактивные формы обеспечивают детальный контроль состояния формы и ее проверку. Этот подход хорошо подходит для сложных форм.
А вот минимальный пример реактивной формы:
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
form = new FormGroup({
name: new FormControl('')
});
onSubmit() {
console.log(this.form.value);
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label for="name">Name:</label>
<input id="name" formControlName="name">
<button type="submit">Submit</button>
</form>
Cигналы как новый способ управления реактивностью форм
С выходом Angular 16 signals стали использоваться как новый способ управления реактивностью. Сигналы обеспечивают декларативный подход к управлению состоянием, делая ваш код более предсказуемым и простым для понимания. При применении к формам сигналы могут повысить простоту форм, управляемых шаблонами, и в то же время обеспечить реактивность и контроль, которые обычно присущи реактивным формам.
Давайте рассмотрим, как сигналы могут использоваться как в простых, так и в сложных сценариях создания форм.
Пример 1: Простая форма signals, управляемая шаблонами
Рассмотрим базовую форму входа в систему. Как правило, это может быть реализовано с использованием форм, управляемых шаблонами, подобных этой:
<!-- login.component.html -->
<form name="form" (ngSubmit)="onSubmit()">
<label for="email">E-mail</label>
<input type="email" id="email" [(ngModel)]="email" required email />
<label for="password">Password</label>
<input type="password" id="password" [(ngModel)]="password" required />
<button type="submit">Login!</button>
</form>
// login.component.ts
import { Component } from "@angular/core";
@Component({
selector: "app-login",
templateUrl: "./login.component.html",
})
export class LoginComponent {
public email: string = "";
public password: string = "";
onSubmit() {
console.log("Form submitted", { email: this.email, password: this.password });
}
}
Этот подход хорошо работает для простых форм, но, вводя сигналы, мы можем сохранить простоту, добавив при этом реактивные возможности:
// login.component.ts
import { Component, computed, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
@Component({
selector: "app-login",
standalone: true,
templateUrl: "./login.component.html",
imports: [FormsModule],
})
export class LoginComponent {
// Define signals for form fields
public email = signal("");
public password = signal(""); // Define a computed signal for the form value
public formValue = computed(() => {
return {
email: this.email(),
password: this.password(),
};
});
public isFormValid = computed(() => {
return this.email().length > 0 && this.password().length > 0;
});
onSubmit() {
console.log("Form submitted", this.formValue());
}
}
<!-- login.component.html -->
<form name="form" (ngSubmit)="onSubmit()">
<label for="email">E-mail</label>
<input type="email" id="email" name="email" [(ngModel)]="email" required email />
<label for="password">Password</label>
<input type="password" name="password" id="password" [(ngModel)]="password" required />
<button type="submit">Login!</button>
</form>
В этом примере поля формы определены как сигналы, позволяющие выполнять реактивные обновления при каждом изменении состояния формы. Сигнал formValue
предоставляет вычисленное значение, отражающее текущее состояние формы. Этот подход предлагает более декларативный способ управления состоянием формы и ее реактивностью, сочетая простоту форм, управляемых шаблонами, с мощью сигналов.
У вас может возникнуть соблазн определить форму непосредственно как объект внутри сигнала. Хотя такой подход может показаться более лаконичным, ввод данных в отдельные поля не приводит к обновлению реактивности, что обычно приводит к нарушению соглашения. Вот пример StackBlitz с компонентом, страдающим от такой проблемы:
Пример можно просмотреть по ссылке: https://stackblitz.com/edit/signal-forms-demo-vesb5d?file=src%2Flogin.component.ts.
Поэтому, если вы хотите реагировать на изменения в полях формы, лучше определить каждое поле как отдельный сигнал. Определяя каждое поле формы как отдельный сигнал, вы гарантируете, что изменения в отдельных полях правильно инициируют обновления реактивности.
Пример 2: Сложная форма с signals
Возможно, вы не увидите особой пользы в использовании сигналов для простых форм, таких как форма входа в систему, описанная выше, но они действительно эффективны при работе с более сложными формами. Давайте рассмотрим более сложный сценарий — форму профиля пользователя, которая включает в себя такие поля, как firstName
, lastName
, email
, phoneNumbers
и address
. Поле phoneNumbers
является динамическим, что позволяет пользователям добавлять или удалять телефонные номера по мере необходимости.
Вот как эта форма может быть определена с помощью сигналов:
// user-profile.component.ts
import { JsonPipe } from "@angular/common";
import { Component, computed, signal } from "@angular/core";
import { FormsModule, Validators } from "@angular/forms";
@Component({
standalone: true,
selector: "app-user-profile",
templateUrl: "./user-profile.component.html",
styleUrls: ["./user-profile.component.scss"],
imports: [FormsModule, JsonPipe],
})
export class UserProfileComponent {
public firstName = signal("");
public lastName = signal("");
public email = signal("");
// We need to use a signal for the phone numbers, so we get reactivity when typing in the input fields
public phoneNumbers = signal([signal("")]);
public street = signal("");
public city = signal("");
public state = signal("");
public zip = signal("");
public formValue = computed(() => {
return {
firstName: this.firstName(),
lastName: this.lastName(),
email: this.email(), // We need to do a little mapping here, so we get the actual value for the phone numbers
phoneNumbers: this.phoneNumbers().map((phoneNumber) => phoneNumber()),
address: {
street: this.street(),
city: this.city(),
state: this.state(),
zip: this.zip(),
},
};
});
public formValid = computed(() => {
const { firstName, lastName, email, phoneNumbers, address } = this.formValue(); // Regex taken from the Angular email validator
const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const isEmailFormatValid = EMAIL_REGEXP.test(email);
return (
firstName.length > 0 &&
lastName.length > 0 &&
email.length > 0 &&
isEmailFormatValid &&
phoneNumbers.length > 0 && // Check if all phone numbers are valid
phoneNumbers.every((phoneNumber) => phoneNumber.length > 0) &&
address.street.length > 0 &&
address.city.length > 0 &&
address.state.length > 0 &&
address.zip.length > 0
);
});
addPhoneNumber() {
this.phoneNumbers.update((phoneNumbers) => {
phoneNumbers.push(signal(""));
return [...phoneNumbers];
});
}
removePhoneNumber(index: number) {
this.phoneNumbers.update((phoneNumbers) => {
phoneNumbers.splice(index, 1);
return [...phoneNumbers];
});
}
}
Обратите внимание, что полеphoneNumbers
определено как сигнал из массива сигналов. Эта структура позволяет нам отслеживать изменения отдельных телефонных номеров и обновлять состояние формы в режиме реального времени. МетодыaddPhoneNumber
иremovePhoneNumber
обновляют массив сигналовphoneNumbers
, запуская обновления реактивности в форме.
<!-- user-profile.component.html -->
<form class="form">
<label for="firstName">First Name</label> <input type="text" id="firstName" name="firstName" [(ngModel)]="firstName" required />
<label for="lastName">Last Name</label> <input type="text" id="lastName" name="lastName" [(ngModel)]="lastName" required />
<label for="email">Email</label> <input type="email" id="email" name="emailAddress" [(ngModel)]="email" required email />
<div id="phoneNumbers">
<label>Phone Numbers</label> @for (phone of phoneNumbers(); track i; let i = $index) {
<div><input type="tel" name="phoneNumbers-{{ i }}" [(ngModel)]="phone" required /> <button type="button" (click)="removePhoneNumber(i)">Remove</button></div>
} <button type="button" (click)="addPhoneNumber()">Add Phone Number</button>
</div>
<label for="street">Street</label> <input type="text" id="street" name="street" [(ngModel)]="street" required />
<label for="city">City</label> <input type="text" id="city" name="city" [(ngModel)]="city" required />
<label for="state">State</label> <input type="text" id="state" name="state" [(ngModel)]="state" required />
<label for="zip">ZIP Code</label> <input type="text" id="zip" name="zip" [(ngModel)]="zip" required />
@if(!formValid()) {
<div class="message message--error">Form is invalid!</div>
} @else {
<div class="message message--success">Form is valid!</div>
}
<pre>
{{ formValue() | json }}
</pre>
</form>
В шаблоне мы используем массив сигналовphoneNumbers
для динамического отображения полей ввода телефонных номеров. МетодыaddPhoneNumber
иremovePhoneNumber
позволяют пользователям автоматически добавлять или удалять телефонные номера, обновляя состояние формы. Обратите внимание на использование функцииtrack
, которая необходима для обеспечения того, чтобы директиваngFor
корректно отслеживала изменения в массивеPhoneNumbers
.
Вот демонстрационный пример сложной формы в StackBlitz, с которым вы можете поиграть% https://stackblitz.com/edit/signal-forms-demo-f5gwur?file=src%2Fuser-profile.component.ts.
Проверка форм с помощью signals
Проверка имеет решающее значение для любой формы, гарантируя, что вводимые пользователем данные соответствуют требуемым критериям перед отправкой. С помощью сигналов проверка может осуществляться реактивным и декларативным образом. В приведенном выше примере сложной формы мы реализовали вычисляемый сигнал под названием formValid
, который проверяет, соответствуют ли все поля определенным критериям проверки.
Логику проверки можно легко настроить в соответствии с различными правилами, такими как проверка допустимых форматов электронной почты или проверка того, что все обязательные поля заполнены. Использование сигналов для проверки позволяет создавать более удобный в обслуживании и тестировании код, поскольку правила проверки четко определены и автоматически реагируют на изменения в полях формы. Его можно даже выделить в отдельную утилиту, чтобы его можно было повторно использовать в разных формах.
В примере со сложной формой сигнал formValid
гарантирует, что все обязательные поля заполнены, и подтверждает формат электронной почты и телефонных номеров.
Этот подход к проверке немного прост и должен быть лучше привязан к реальным полям формы. Хотя он будет работать во многих случаях, в некоторых случаях вы можете захотеть подождать, пока в Angular не будет добавлена явная поддержка signal forms.
Зачем использовать сигналы в формах Angular?
Использование сигналов в Angular предоставляет новый мощный способ управления состоянием формы и ее реактивностью. Сигналы предлагают гибкий декларативный подход, который может упростить обработку сложных форм, сочетая преимущества форм, управляемых шаблонами, и реактивных форм. Вот некоторые ключевые преимущества использования сигналов в формах Angular.:
- Декларативное управление состоянием: Сигналы позволяют вам декларативно определять поля формы и вычисляемые значения, что делает ваш код более предсказуемым и понятным.
- Реактивность: Сигналы обеспечивают реактивное обновление полей формы, гарантируя, что изменения в состоянии формы автоматически вызовут обновление реактивности.
- Детальное управление: Сигналы позволяют определять поля формы на детальном уровне, обеспечивая детальный контроль состояния формы и ее проверку.
- Динамические формы: Сигналы можно использовать для создания динамических форм с полями, которые можно динамически добавлять или удалять, обеспечивая гибкий способ обработки сложных сценариев формирования.
- Простота: Сигналы могут предложить более простой и лаконичный способ управления состояниями форм, чем традиционные реактивные формы, что упрощает создание и поддержку сложных форм.
Вывод
С появлением signals разработчики Angular получили новый инструмент, который сочетает в себе простоту форм, управляемых шаблонами, с реактивностью реактивных форм.
В то время как многие варианты использования требуют использования реактивных форм, сигналы предоставляют новую, мощную альтернативу для управления состоянием форм в приложениях Angular, требующих более простого декларативного подхода. Поскольку Angular продолжает развиваться, эксперименты с этими новыми функциями помогут вам создавать более удобные в обслуживании и производительные приложения.
Счастливого написания кода!