Complex forms with
advanced directives in
AngularJS
By: Matias Niemelä
matias [at] yearofmoo [dot] com
Slides, Code & Demo
Slides: yom.nu/ng-mtv-forms-meetup
Code: yom.nu/ng-forms-refactor-demo
Demo:
yom.nu/ng-survey-creator-demo
Form Development in a Nutshell
We create a bunch of fields
The user enters data
Then validation is performed
And we push forward
Example HTML code
<form id="some-form">
<div class="field">
<label>Some Input</label>
<input type="text" id="some-field" name="some_field" />
</div>
<hr />
<input type="submit" value="Submit the form" />
</form>
Building Forms with Basic JavaScript
User enters something
At some point we collect and submit the info
Via AJAX or by page post
Lots of JS code to validate things
Example JS code
$(function() {
$('#some-form').on('submit', function(event) {
event.preventDefault();
var field = $('#some-field');
var value = field.val();
var valid = value && value.length;
if (valid) { alert('send to the server'); }
});
});
Example demo of a form
Form development is painful
Wayyy to much coupling between the JS and HTML
Dynamically updating content?
Validations are tricky
What about AJAX form submission?
Animation? Notifications? The UX?
What about using Angular for forms?
Angular uses more HTML code
To better express the structure of a form
The scoping system allows for data to be separate
And the directives allow for dynamic code + validation
Forms in AngularJS
Use standard HTML5 Markup
With ngModel as the bridge
Then DOM events to submit the data
And directives to enhance it all
The basic building blocks for NG forms
The data
The components
The logic
The presentation
The data
NgModel => two-way data binding
<form name="myForm">
<label>Email:</label> <br />
<input type="text" ng-model="data.email" name="myEmail" /> <br />
<label>Description:</label> <br />
<textarea ng-model="data.description" name="myDescription"></textarea>
</form>
The data (demo)
The components
We use ng-model with input, textarea or select fields
(which abstracts the input.value nonsense)
The validation attributes place constrictions (like "email")
And only "valid" data is then written to scope
The components (example)
Angular supports a number of input types
<input type="email" ng-model="data.email" name="myEmail" />
<input type="number" ng-model="data.age" name="myAge" />
<input type="date" ng-model="data.birthday" name="myBirthday" />
<input type="text" ng-model="data.username"
name="myUsername"
username-characters-validator
username-availability-validator />
The logic
We can use logic to change and/or to validate data
We can also use $parsers and $formatters to shape data
Or we can do something crazy in our controller code
The logic (input)
<form name="myForm" ng-controller="MyFormCtrl as ctrl" ng-submit="ctrl.submit(data)">
<label>Location (City):</label>
<input type="text"
ng-model="data.location"
name="myLocation"
location-input-parser />
<!-- continued on the next slide -->
The logic (html)
<!-- inside of the form -->
<div ng-if="data.location && data.location.country != 'USA'">
<label>
<input ng-model="data.localCurrency"
type="checkbox"
value="true"
name="myCurrency" />
Do you wish to use USD or your local currency?
</label>
</div>
</form>
The logic (directive)
ngModule.directive('locationInputParser', function(locationParser) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
return locationParser.parse(value);
});
ngModel.$formatters.push(function(location) {
return location && location.title;
});
}
}
})
The logic (controller)
ngModule.controller('MyFormCtrl', function() {
this.submit = function(data) {
alert('data is being sent to the server');
}
});
The presentation
NgModel exposes flags
($invalid, $touched, $error, etc...)
We add/remove components with them
Or errors
plus apply animations
The presentation (example)
<form name="myForm">
<label>Email Address:</label>
<input type="email" name="myEmail"
ng-model="data.email"
required
minlength="5"
maxlength="100"
email-address-availability-validator />
<!-- code continues to next slide ... -->
The presentation (example)
<div ng-if="myForm.myEmail.$invalid">
<div ng-if="myForm.myEmail.$error.required">
You did not enter your email
</div>
<div ng-if="myForm.myEmail.$error.minlength">
Your email is too short
</div>
<div ng-if="myForm.myEmail.$error.maxlength">
Your email is too long
</div>
<div ng-if="myForm.myEmail.$error.email">
Your email is invalid
</div>
<div ng-if="myForm.myEmail.$error.emailAddressAvailability">
Your email is already in use by another user...
</div>
</div>
</form>
Once the form is working...
We should refactor the HTML
Let's use directives
Which makes things reusable
Cleaner HTML == Better Comprehension
Testing is also easier
Example: A Survey Creator in Angular
yom.nu/ng-forms-refactor-demo
A survey to create surveys
This means...
Dynamic validations, fields & data
What are the challenges?
Structure of the HTML?
What directives to use?
How can this be flexible?
What to avoid?
Getting started
First define a form
... for the survey creator
Getting started
<form ng-controller="SurveyFormController as ctrl"
name="surveyForm"
ng-submit="ctrl.submit(surveyForm.$valid, data)"
novalidate>
...
</form>
The "controller" for the form
Let's start out by making a controller for the form
The submit function
The data comes in as an object
The form doesn't care about the data
The "controller" for the form
.controller("SurveyFormController", [function() {
this.submit = function(isValid, data) {
if (isValid) {
//save and redirect
}
};
}])
The repeated regions
Let's use ng-repeat to repeat things out
And repeat on the data
We're still using a single object as an entry point
The repeated regions
<!-- inside of the form -->
<div class="repeated-form-fields"
ng-form="surveyFieldsForm"
ng-controller="SurveyFieldsFormController as fieldsCtrl"
fields="data.fields">
...
</div>
The repeated regions
.controller("SurveyFieldsFormController",
['$attrs', '$scope', function($attrs, $scope) {
var fields = $scope.$eval($attrs.fields);
this.newField = function() { fields.push({}) };
this.inputTypes = [...]
this.removeField = function() { ... };
//isMultipleField, allowSwap, swapFields...
}])
The dynamic fields
<!-- inside of the fields repeater -->
<div ng-form="surveyFieldForm"
ng-repeat="field in data.fields track by $index"
class="repeated-form-row row">
...
</div>
The fields & error messages
Inner form == Dynamic field scoping
question label = surveyFieldForm.label
question type = surveyFieldForm.type
The error messages
Use ngMessages for form errors
(this is possible with the nested form)
<!-- inside of the repeated field region -->
<div class="errors" ng-messages="surveyFieldForm.field.$error">
<div ng-message="required">You did not enter a field value</div>
</div>
<div class="errors" ng-messages="surveyFieldForm.type.$error">
<div ng-message="required">You did not enter a type value</div>
</div>
The Preview Page
We need a page to display the questions
And then to collect the survey data
The Preview Page
We loop over the questions
<div ng-controller="SurveyPreviewController as previewCtrl">
<form name="previewForm"
ng-submit="previewCtrl.submit(previewForm.$valid, data)">
<div ng-form="answerEntryForm"
ng-repeat="field in previewCtrl.survey.fields track by $index">
...
</div>
</form>
</div>
The actual survey fields
Then we display each value inline?
<div ng-switch="field.type">
<div ng-switch-when="input">
<input type="text" ng-model="field.answer" />
</div>
<div ng-switch-when="select">
<select ng-model="field.answer">...</select>
</div>
</div>
The first draft is complete? now what?
Refactor the controllers
Refactor the HTML with directives
Package everything up
Test it out
Improving everything with directives
The goal is reduce the HTML
To abstract it into smaller chunks
That are more reusable and easier to read
Think of directives as functions in JavaScript
Refactor 1: Remove data from logic
Too much of a mix of data + logic
Use directives + attributes to reduce
Assume each directive knows nothing else
Refactor 1: Remove data from logic
Don't mix this stuff up
.controller("SurveyFormController", [function() {
// THIS IS DATA
$scope.data = surveyData;
$scope.data.fields = $scope.data.fields || [];
var entryExists = surveyData.id >= 0;
var ctrl = this;
// THIS IS LOGIC
this.submit = function(isValid, data) {
Refactor 2: Directives instead of controllers
ng-controller is too much code
Better to use a controller that exposes itself
The Editor Page
Create some nice HTML code to house everything
<survey-editor-form model="data" on-submit="...">
<survey-editor-fields fields="data.fields[$index]">
<survey-editor-field ng-repeat="field in data.fields">
...
</survey-editor-field>
</survey-editor-fields>
</survey-editor-form>
The Preview Page
Create some nice HTML code to house everything
<survey-preview-form model="data" on-submit="...">
<survey-preview-field
ng-repeat="field in survey.fields"
type="field.type"
model="data.answers[$index]">
...
</survey-editor-field>
</survey-editor-form>
Refactor 3: Package it up
What about the big ng-switch?
We can package more directives
And map in the attributes
Refactor 3: Package it up
<div ng-switch="field.type">
<div ng-switch-when="input">
<app-input-text-component field="field" model="data.answers[$index]">
</app-input-text-component>
</div>
<div ng-switch-when="input:url">
<app-input-url-component field="field" model="data.answers[$index]">
</app-input-url-component>
</div>
...
</div>
Refactor 3: Package it up
ngModule.directive(directiveSelector, [function() {
return {
controller: 'InputComponentController',
controllerAs: 'componentCtrl',
templateUrl : './field-templates/select.html',
scope: {
model: '='
}
}
}]);
Refactor 3: Package it up
<select class="input select"
ng-model="componentCtrl.model"
ng-options="option as option for option in componentCtrl.options"
ng-required="componentCtrl.required">
</select>
Test it out
The more directives we have
... the smaller the tests
The less complex the directives
... the less we need to prepare
Test it out
it('should expose a form which collects questions', function() {
var element = angular.element(
'<survey-editor-form model="data"></div>');
expect(element.scope().surveyForm).toBeTruthy();
});
it('should expose a survey field', function() {
var element = angular.element('<survey-editor-field ...></survey-editor-field>');
//...
});
What did I miss?
Look at ngMessages
Look at async + sync validation in 1.3
ngModelOptions
More info here...
Questions? (maybe some answers)
What do you think about using more directives?
Do you like cleaner HTML?
That's it for now...