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...

Name: Matias Niemelä

Twitter: @yearofmoo

Website: www.yearofmoo.com

Email: matias [at] yearofmoo [dot] com