В этом посте мы увидим, как работает API Angular 2 Forms и как его можно использовать для создания сложных форм. Мы рассмотрим следующие темы на основе примеров, доступных в этом хранилище :
- Что такое Angular 2 Forms
- Шаблон управляемых форм, или угловой 1 способ
- Модель Driven Forms, или новый функционально-реактивный способ
- Преимущества и недостатки обоих типов форм
Angular 2 Forms — О чем они все?
Большая категория веб-приложений очень интенсивно использует формы, особенно в случае развития предприятия. Многие из этих приложений представляют собой просто огромные формы, охватывающие несколько вкладок и диалогов и имеющие нетривиальную бизнес-логику проверки.
Каждое приложение с интенсивной формой должно предоставить ответы на следующие проблемы:
- Как отслеживать состояние глобальной формы
- Знать, какие части формы действительны, а какие еще недействительны
- Правильное отображение сообщений об ошибках для пользователя, чтобы он знал, что нужно сделать, чтобы сделать форму действительной
Все это нетривиальные задачи, которые похожи в разных приложениях, и поэтому могут выиграть от каркаса.
Платформа Angular 2 предоставляет нам две альтернативные стратегии для работы с формами, и мы должны решить, что лучше всего подходит для нашего проекта.
Угловые 2 шаблонно-управляемые формы
Angular 1 справляется с формами через известную ng-model
директиву (подробнее об этом в этом посте ).
Мгновенное двустороннее связывание данных ng-model
в Angular 1 действительно спасает жизнь, поскольку оно позволяет прозрачно синхронизировать форму с моделью представления. Формы, созданные с помощью этой директивы, могут быть проверены только в сквозном тестировании, поскольку для этого требуется наличие DOM, но все же этот механизм очень полезен и прост для понимания.
Angular 2 предоставляет аналогичный механизм, также называемый ng-model
, который позволяет нам создавать то, что сейчас называется шаблонно-управляемыми формами. Давайте посмотрим на форму, построенную с использованием этого:
<section class="sample-app-content">
<h1>Template-driven Form Example:</h1>
<form #f="form" (ng-submit)="onSubmitTemplateBased()">
<p>
<label>First Name:</label>
<input type="text" ng-control="firstName"
[(ng-model)]="vm.firstName" required>
</p>
<p>
<label>Password:</label>
<input type="password" ng-control="password"
[(ng-model)]="vm.password" required>
</p>
<p>
<button type="submit" [disabled]="!f.valid">Submit</button>
</p>
</form>
</section>
На самом деле в этом простом примере происходит довольно много. Здесь мы объявили простую форму с двумя элементами управления: имя и пароль, оба из которых являются обязательными.
Форма запускает метод контроллера onSubmitTemplateBased
при отправке, но кнопка отправки активна, только если заполнены оба обязательных поля. Но это только малая часть того, что здесь происходит.
Угловые 2 формы из коробки функциональность
Обратите внимание на использование [(ng-model)]
этой нотации, которая подчеркивает, что два элемента управления форм связаны в двух направлениях с переменной модели представления, названной в стиле Angular 1 как просто vm
.
Более того, когда пользователь щелкает по обязательному полю, поле отображается красным, пока пользователь что-то не введет. Angular фактически отслеживает три состояния полей формы и применяет классы CSS для каждого к форме и ее элементам управления:
- тронутый или нетронутый
- действительный или недействительный
- нетронутый или грязный
Эти классы состояний CSS очень полезны для стилизации состояний ошибок формы.
Angular также отслеживает состояние достоверности всей формы, используя его для включения / выключения кнопки отправки. Эта функциональность на самом деле является общей для шаблонов и форм.
Логика для всего этого должна быть в контроллере, верно?
Давайте посмотрим на контроллер, связанный с этим представлением, чтобы увидеть, как реализована вся эта обычно используемая логика форм:
@Component({
selector: "template-driven-form"
})
@View({
templateUrl: 'template-driven-form.html',
directives: [FORM_DIRECTIVES]
})
export class TemplateDrivenForm {
vm: Object = {};
onSubmitTemplateBased() {
console.log(this.vm);
}
}
Не так много, чтобы увидеть здесь! У нас есть только объявление для объекта модели представления vm
и используемый обработчик событий ng-submit
.
Все очень полезные функции отслеживания ошибок формы и регистрации валидаторов позаботятся о нас, просто включив их FORM_DIRECTIVES
в наш список директив!
Как Angular тянет это?
Это работает так, что при FORM_DIRECTIVES
применении к представлению Angular автоматически применяет ControlGroup
директиву к элементу формы прозрачным способом.
Если по какой-то причине вы этого не хотите, вы всегда можете отключить эту функцию, добавив ее ng-no-form
в качестве атрибута формы.
Кроме того, к каждому ng-control
также будет применена директива, которая будет регистрироваться в контрольной группе, и валидаторы будут зарегистрированы, если такие элементы как required
или maxlength
применяются к ng-control
.
Присутствие [(ng-model)]
также зарегистрирует директиву, которая подключит двунаправленную привязку между формой и моделью представления, и, в конце концов, на уровне контроллера больше ничего не нужно делать.
Вот почему это называется шаблонно-управляемыми формами, потому что вся логика проверки объявлена в шаблоне. Это почти идентично тому, как это делается в Angular 1.
Преимущества и недостатки шаблонно-управляемых форм
В этом простом примере мы на самом деле не можем его увидеть, но сохранение шаблона в качестве источника всей истины проверки формы — это то, что может стать довольно волосатым довольно быстро.
По мере того, как мы добавляем все больше и больше тегов валидаторов в поле или когда мы начинаем добавлять сложные валидации между полями, читаемость формы снижается до такой степени, что ее будет сложнее передать ее веб-дизайнеру.
Преимуществом этого способа обработки форм является его простота, и его, вероятно, более чем достаточно для создания очень большого диапазона форм.
С другой стороны, логика проверки формы не может быть проверена модулем. Единственный способ проверить эту логику — это запустить сквозное тестирование с помощью браузера, например, с использованием безголового браузера PhantomJs
.
Шаблонно-ориентированные формы с точки зрения функционального программирования
В формах, управляемых шаблонами, нет ничего плохого, но с точки зрения техники программирования это решение, которое способствует изменчивости.
Each form has a state that can be updated by many different interactions and its up to the application developer to manage that state and prevent it from getting corrupted. This can get hard to do for very large forms and can introduce a whole category of potential bugs.
Inspired by what was going on in the React world, the Angular 2 team added a different alternative for managing forms, so let’s go through it.
Model Driven Forms
A model driven form looks on the surface pretty much like a template driven form. Let’s take our previous example and re-write it:
<section class="sample-app-content">
<h1>Model-based Form Example:</h1>
<form [ng-form-model]="form" (ng-submit)="onSubmit()">
<p>
<label>First Name:</label>
<input type="text" ng-control="firstName">
</p>
<p>
<label>Password:</label>
<input type="password" ng-control="password">
</p>
<p>
<button type="submit" [disabled]="!form.valid">Submit</button>
</p>
</form>
</section>
There are a couple of differences here. first there is a ng-form-model
directive applied to the whole form, binding it to a controller variable named form
.
Notice also that the required
validator attribute is not applied to the form controls. This means the validation logic must be somewhere in the controller, where it can be unit tested.
What Does the Controller Look Like?
There is a bit more going on in the controller of a Model Driven Form, let’s take a look at the controller for the form above:
@Component({
selector: "model-driven-form"
})
@View({
templateUrl: 'model-driven-form.html',
directives: [FORM_DIRECTIVES]
})
export class ModelDrivenForm {
form: ControlGroup;
firstName: Control = new Control("", Validators.required);
constructor(fb: FormBuilder) {
this.form = fb.group({
"firstName": this.firstName,
"password":["", Validators.required]
});
}
onSubmitModelBased() {
console.log("model-based form submitted");
console.log(this.form);
}
}
We can see that the form is really just a ControlGroup
, which keeps track of the global validity state. The controls themselves can either be instantiated individually or defined using a simplified array notation using the form builder.
In the array notation, the first element of the array is the initial value of the control, and the remaining elements are the control’s validators. In this case both controls are made mandatory via the Validators.required
built-in validator.
But What Happened to ng-model
?
Note that ng-model
can still be used with model driven forms. Its just that the form value would be available in two different places: the view model and the ControlGroup
, which could potentially lead to some confusions.
Advantages and Disadvantages of Model Driven Forms
You are probably wondering what we gained here. On the surface there is already a big gain:
We can now unit test the form validation logic !
We can do that just by instantiating the class, setting some values in the form controls and perform assertions against the form global valid state and the validity state of each control.
But this is really just the tip of the iceberg. The ControlGroup
and Control
classes provide an API that allows to build UIs using a completely different programming style known as Functional Reactive Programming.
Functional Reactive Programming in Angular 2
This deserves it’s own blog post, but the main point is that the form controls and the form itself are now Observables. You can think of observables simply as streams.
This mean that both the controls and the whole form itself can be viewed as a continuous stream of values, that can be subscribed to and processed using commonly used functional primitives.
For example, its possible to subscribe to the form stream of values using the Observable API like this:
this.form.valueChanges.toRx()
.map((value) => {
value.firstName = value.firstName.toUpperCase();
return value;
})
.filter((value) => this.form.valid)
.subscribe((value) => {
alert("View Model = " + JSON.stringify(value));
});
What we are doing here is taking the stream of form values (that changes each time the user types in an input field), and then apply to it some commonly used functional programming operators: map
and filter
.
Note: The
toRx()
part will go away in the near future
In fact, the form stream provides the whole range of functional operators available in Array
and many more.
In this case we are converting the first name to uppercase using map
and taking only the valid form values using filter
. This creates a new stream of modified valid values to which we subscribe, by providing a callback that defines how the UI should react to a new valid value.
Advantages of Building UIs Using Functional Reactive Programming (FRP)
We are not obliged to use FRP techniques with Angular 2 Model Driven Forms. Simply using them to make the templates cleaner and allow for component unit testing is already a big plus.
But the use of FRP can really allow us to completely change the way we build UIs. Imagine a UI layer that basically holds no state for the developer to manage, there are really only streams of either browser events, backend replies or form values binding everything together.
This could potentially eliminate a whole category of bugs that come from mutability and corrupted application state. Building everything around the notion of stream might take some getting used it and probably reaps the most benefit in the case of more complex UIs.
Also FRP techniques can help easily implement many use cases that would otherwise be hard to implement such as:
- pre-save the form in the background at each valid state
- typical desktop features like undo/redo
Summary
Angular 2 provides two ways to build forms: Template Driven and Form Driven, both with their advantages and disadvantages.
The Template Driven approach is very familiar to Angular 1 developers, and is ideal for easy migration of Angular 1 applications into Angular 2.
The Model Driven approach provides the immediate benefit ot testability and removes validation logic from the template, keeping the templates clean of validation logic. But also allows for a whole different way of building UIs that we can optionally use, very similar to what is available in the React world.
Its really up to us to assess the pros and cons of each approach, and mix and match depending on the situation choosing the best tool for the job at hand.
Related Links
If you want to know more about Angular 2 Forms, the podcast of Victor Savkin on Angular Air goes into detail on the two form types and ng-model
.
This blog post gives a high level overview of how Angular 2 will better enable Functional Reactive Programming techniques.
If you are interested in learning about how to build components in Angular 2, check also The fundamentals of Angular 2 components