Использование сигналов при работе с формами в Angular
Signal Forms
основаны на существующей проверенной функциональности. В шаблоне они используют ngModel
для удобства и эффективности. Это позволяет поддерживать синхронизацию шаблона и модули. На уровне TypeScript у Signal Forms
аналогичный API с реактивными формами, дополненный некоторыми утилитами, и используют сигналы вместо RxJS
. Благодаря схожему API, переход от реактивных форм к Signal Forms
должен быть простым при наличии интереса и мотивации.
Начало работы
Для начала работы с Signal Forms
достаточно выполнить команду:
FormField<T>
Форма построенная на сигналах создается с использованием методов createFormField
и createFormGroup
. Самый простой пример - это поле формы FormField
, которое создается с помощью createFormField
.
import { createFormField } from 'ng-signal-forms'; export class AppComponent { // implicit type name = createFormField(''); // or be explicit about the type name = createFormField<string>(''); }
Для создания поля формы используется метод createFormField
. Первый аргумент этого метода задает начальное значение поля, что определяет его тип. Для привязки поля к элементу HTML используются атрибуты ngModel
и formField
.
<div> <label for='name'>Name</label> <input ngModel [formField]='name' id='name' /> </div>
FormField<T> States
Как упомянуто ранее, атрибут formField
является связующим элементом между моделью и элементом HTML
. Это обеспечивает синхронизацию значения и состояния, отражая внутреннее состояние формы через соответствующие классы и атрибуты HTML
. В данном случае контрол отвечает за установку классов:
Signal Forms
, следуя конвенции официальных форм Angular
, также устанавливают следующие атрибуты:
FormGroup<T>
Для объединения полей в форму (или группу) используется метод createFormGroup
. Объект, передаваемый в этот метод, содержит ключи с именами полей и соответствующие значения - сами поля формы.
import { createFormField, createFormGroup } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ name: createFormField(''), bibNumber: createFormField<number | undefined>(undefined), }); }
Для простых форм без дополнительных опций можно использовать сокращенный способ создания полей. Вместо createFormField
можно сразу передать начальное значение.
import { createFormGroup } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ name: '', bibNumber: undefined, }); }
Для использования formModel
в HTML-шаблоне, обратитесь к полям формы через свойство controls
модели формы.
<div> <label for='name'>Name</label> <input ngModel [formField]='formModel.controls.name' id='name' /> </div> <div> <label for='bib'>Bib number</label> <input type="number" ngModel [formField]='formModel.controls.bibNumber' id='bib' /> </div>
Группы форм могут быть вложенными. Для этого используйте метод createFormGroup
. В HTML обращайтесь к вложенной группе форм через свойство controls
.
import { createFormField, createFormGroup } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ address: createFormGroup({ street: createFormField(''), }), }); }
<fieldset> <legend>Address</legend> <div> <label for='street'>Street</label> <input ngModel [formField]='formModel.controls.address.controls.street' id='street' /> </div> </fieldset>
Метод createFormGroup
принимает как объект, так и массив формовых полей (или групп форм). В HTML-шаблоне используйте метод controls
для итерации по полям формы и привязки каждого контрола к элементу ввода.
import { createFormField, createFormGroup } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ team: createFormGroup([createFormField(''), createFormField(''), createFormField('')]), }); }
<fieldset> <legend>Team members</legend> @for (member of formModel.controls.team.controls(); track $index) { <div> <label for='member-{{$index}}'>Team member {{$index}}</label> <input ngModel [formField]='member' id='member-{{$index}}' /> </div> } </fieldset>
FormGroup<T> State
Состояния FormGroup
идентичны состояниям полей, за исключением того, что FormGroup
не устанавливают классы или атрибуты на HTML
-элементе.
Состояние формы вычисляется на основе состояния ее полей. Если хотя бы одно поле изменено (dirty
), состояние всей формы так же пометится как измененное. Если хотя бы одно поле былоtouched
, состояние группы форм также устанавливается как touched
.
Исключение составляет валидация, которая зависит как от группы, так и от валидности ее полей. FormGroup не обладает состояниями disabled
или read-only
, так как они не применимы к группе форм.
Validators
Форма бесполезна без валидации. Signal Forms
предоставляют ряд встроенных валидаторов, которые могут использоваться для проверки полей и форм.
Для регистрации валидатора достаточно передать массив валидаторов в параметры поля. Стандартные валидаторы доступны через экспорт V
.
Для отображения сообщения об ошибке пользователю используется метод hasError
контрола. Приведенный ниже пример использует touched
для отображения ошибки только при взаимодействии пользователя с полем.
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ name: createFormField('', { validators: [V.required()], }), }); }
<div> <label for='name'>Name</label> <input ngModel [formField]='formModel.controls.name' id='name' /> @if (formModel.controls.name.touched()) { @if (formModel.controls.name.hasError('required')) { <p class="text-[0.8rem] font-medium text-destructive">Name is required.</p> } } </div>
Валидационные сообщения могут быть показаны пользователю, если предоставить сообщение об ошибке валидатору. Это удобнее сделать на этом этапе, чем в шаблоне HTML, а также позволяет повторно использовать сообщения в различных формах.
Для добавления сообщения к валидатору передайте объект с сообщением в сам валидатор. В этом объекте свойство message
устанавливается как функция, возвращающая сообщение. Функция принимает конфигурацию валидатора
в качестве аргумента, что позволяет создавать динамические сообщения.
Сообщение о валидации можно получить, используя метод errorMessage
контрола формы.
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ name: createFormField('', { validators: [ V.required(), { validator: V.minLength(2), message: ({ minLength }) => `Name must be at least ${minLength} characters`, }, ], }), }); }
<div> <label for='name'>Name</label> <input type='text' id='name' ngModel [formField]='formModel.controls.name' hlmInput /> @if (formModel.controls.name.touched()) { @if (formModel.controls.name.hasError('required')) { <p class="text-[0.8rem] font-medium text-destructive">Name is required.</p> } @else if (formModel.controls.name.hasError('minLength')) { <p class="text-[0.8rem] font-medium text-destructive"> {{ formModel.controls.name.errorMessage('minLength') }} </p> } } </div>
Валидация основанная на других полях
В некоторых случаях валидация зависит от значения другого поля. Для этого можно использовать value
, чтобы реагировать на изменения другого поля. Так как значение другого поля является сигналом, валидатор автоматически обновится при его изменении.
Простейший способ - создать FormGroup
и ссылаться на поля.
На стороне HTML необходимо оставить все без изменений (сообщения о валидации опущены для краткости).
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ passwords: createFormGroup(() => { const password = createFormField('', { validators: [V.required()], }); const passwordConfirmation = createFormField('', { validators: [V.equalsTo(password.value)], }); return { password, passwordConfirmation, }; }), }); }
<div> <label for='password'>Password</label> <input type='password' ngModel [formField]='formModel.controls.passwords.controls.password' id='password' /> </div> <div> <label for='password-confirmation'>Password Confirmation</label> <input type='password' ngModel [formField]='formModel.controls.passwords.controls.passwordConfirmation' id='password-confirmation' /> </div>
Чтобы отключить валидатор, используйте свойство disabled в конфигурации валидатора.
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ shipping: createFormGroup(() => { const differentFromBilling = createFormField(false); return { differentFromBilling, street: createFormField('', { validators: [ { validator: V.required(), disable: () => !differentFromBilling.value(), }, ], }), zip: createFormField('', { validators: [ { validator: V.required(), disable: () => !differentFromBilling.value(), }, ], }), }; }), }); }
<section> <div> <h3>Shipping address</h3> <div> <label> <input type="checkbox" ngModel [formField]='formModel.controls.shipping.controls.differentFromBilling' id='differentFromBilling' /> Is different from billing address? </label> </div> </div> <div> <div> <label for='street'>Street</label> <input ngModel [formField]='formModel.controls.shipping.controls.street' id='street' /> </div> <div> <label for='zip'>Zip</label> <input ngModel [formField]='formModel.controls.shipping.controls.zip' id='zip' /> </div> </div> </section>
Дополнительные состояния
Hidden
В примере с отключением валидаторов показано, как можно отключать валидаторы на основе значений других полей. Чтобы обеспечить лучший пользовательский опыт, Signal Forms
также учитывают "скрытое" состояние элементов управления, которое можно использовать для скрытия HTML-элементов.
Для скрытия поля формы установите свойство hidden
в опции поля формы. Мы можем использовать сигналы для реактивного скрытия поля формы на основе других источников сигналов, например, другого поля формы.
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { formModel = createFormGroup({ shipping: createFormGroup(() => { const differentFromBilling = createFormField(false); return { differentFromBilling, street: createFormField('', { hidden: () => !differentFromBilling.value(), validators: [ { validator: V.required(), disable: () => !differentFromBilling.value(), }, ], }), zip: createFormField('', { hidden: () => !differentFromBilling.value(), validators: [ { validator: V.required(), disable: () => !differentFromBilling.value(), }, ], }), }; }), }); }
<section> <div> <h3>Shipping address</h3> <div> <label> <input type="checkbox" ngModel [formField]='formModel.controls.shipping.controls.differentFromBilling' id='differentFromBilling' /> Is different from billing address? </label> </div> </div> <div> <!-- using the control flow API --> @if(!formModel.controls.shipping.controls.street.hidden()){ <div> <label for='street'>Street</label> <input ngModel [formField]='formModel.controls.shipping.controls.street' id='street' /> </div> } <!-- using the hidden attribute--> <div [hidden]="formModel.controls.shipping.controls.zip.hidden()"> <label for='zip'>Zip</label> <input ngModel [formField]='formModel.controls.shipping.controls.zip' id='zip' /> </div> </div> </section>
Состояния поля FormField<T>
автоматически приводят к установке атрибутов disabled
и readonly
на соответствующем HTML-элементе. Однако способы управления этими состояниями со стороны формы не рассматривались ранее.
Для установки состояний disabled
и readonly
для поля формы можно использовать свойства disabled
и readonly
в параметрах контрола. Поскольку HTML-элемент привязан к полю формы, он автоматически отражает текущее состояние поля.
import { createFormField, createFormGroup, V } from 'ng-signal-forms'; export class AppComponent { readonlyBibNumber = signal(false); formModel = createFormGroup({ bibNumber: createFormField<number | undefined>(4706674, { readOnly: () => this.readonlyBibNumber(), }), }); }
Это поведение можно отключить, установив атрибут propagateState
в значение false
.
<input type='number' ngModel [formField]='formModel.controls.bibNumber' id='bib' [propagateState]="false" />
Работа со значениями форм
Для получения значения поля или группы можно использовать сигнал value
.
export class AppComponent { formModel = createFormGroup({...}) protected submit(): void { console.log(this.formModel.value()) // or from a specific field console.log(this.formModel.controls.shipping.controls.street.value()) } }
При отправке формы можно также проверить состояние валидации и формы, используя сигнал valid
.
export class AppComponent { formModel = createFormGroup({...}) protected submit(): void { if(this.formModel.valid()){ console.log(this.formModel.value()) } } }
Пока что не так просто обновить FormGroup. Как вариант, можно установить значение отдельного поля. Поскольку мы работаем с сигналами, мы можем использовать методы set и update для обновления значения поля.
export class AppComponent { formModel = createFormGroup({...}) protected setValue(): void { this.formModel.controls.name.value.set('John Doe'); this.formModel.controls.bibNumber.value.update(v => v*10); } }
Полезно знать
Использвование существующей ngModel
Поскольку Signal Forms
построены на базе существующего NgModel
, возможно использовать функции в сочетании с Signal Forms
. Например, атрибут ngModelOptions
можно использовать для установки стратегии обновления поля формы, в приведенном ниже примере значение поля формы обновляется при потере фокуса.
<input id="username" ngModel [formField]="formModel.controls.name" [ngModelOptions]="{updateOn: 'blur'}"/>
Для отладки формы можно использовать effect
для регистрации значения и состояния формы при изменении.
export class AppComponent { formModel = createFormGroup({...}); debug = effect(() => { console.log('value:', this.formModel.value()); console.log('valid:', this.formModel.valid()); }) }
Еще один трюк - использовать json pipe для регистрации значения и состояния формы в HTML-шаблоне.
<pre> {{ { value: formModel.value(), touchState: formModel.touchedState(), dirtyState: formModel.dirtyState(), valid: formModel.valid(), errors: formModel.errorsArray(), } | json }} </pre>
Заключение
Signal Forms
представляет новый метод создания форм в Angular, основанный на существующем NgModel
с API, аналогичным ReactiveForms
. Он использует сигналы для создания реактивных и гибких форм. Благодаря реактивной природе сигналов, достигается простота создания сложных форм.
Сигналы подходят для этой задачи из-за их реактивного механизма. Форма может реагировать на изменения значений для запуска соответствующих валидаторов.
Ключевая идея Signal Forms
заключается в том, что вся логика находится внутри модели формы. Поэтому добавлены дополнительные состояния, такие как hidden
, enable/disable
и readonly
. HTML отвечает только за отображение текущего состояния формы.
Результатом является эффективная, гибкая и реактивная форма.
Пример доступен по ссылке. Больше примеров тут
Это вольный перевод оригинального материала