Posts Tagged Directive

Data-Driven Forms with AngularJS’s Two-Way Data Binding and Custom Directives

Use the two-way data binding and custom directives features of AngularJS to develop data-driven, interactive forms.

Introduction

AngularJS has exploded on to the web-application development scene. Since being introduced in 2009, AngularJS’s use has grown exponentially. Its wide range of features and ease of use make it an ideal tool for rapidly developing modern web-applications. Combined with other modern JavaScript tools, such as Node, Express, Twitter Bootstrap, Yeoman, and NoSQL databases such as MongoDB, AngularJS developers can create robust, full-stack JavaScript applications.

A primary feature of AngularJS is two-way data binding. According to AngularJS’s website, ‘data-binding is the automatic synchronization of data between the model and view. The way that Angular implements data-binding lets you treat the model as the single-source-of-truth in your application. The view is a projection of the model at all times. When the model changes, the view reflects the change, and vice versa.‘ In the past, developers spent much of their coding time wiring up UI components to the application’s data model. AngularJS has greatly simplified this process.

Another key feature of AngularJS are directives. At a high level, according to AngularJS’ site, ‘directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS’s HTML compiler to attach a specified behavior to that DOM element or even transform the DOM element and its children.‘ AngularJS provides many built-in directives, including ngModel, ngBind, ngInclude, ngRepeat, and ngChange. These directives are the building blocks of an AngularJS application. We will use many of these built-in directives in this post.

In addition to built-in directives, AngularJS allows us to create custom directives. Custom directives are a powerful feature, allowing us to encapsulate our own reusable DOM manipulation functionality.

The Sample Project

There is an infinite variety of web-based forms (‘electronic forms’). We interact with web-based forms at work, at home, and at school. Forms serve the primary purpose of collecting data user. Web-based forms allow us to order products and services over the internet, file our taxes, manage our benefits at work, track our time, and take online classes.

Tests or quizzes are a perfect example of web-based forms to demonstrate AngularJS’s many strengths, including data-binding and custom directives. In this post, we will create a series of interactive quizzes on the theme of AngularJS – sort of a learning opportunity inside a learning opportunity. Quizzes often contain several common types of question/answer formats, including true-false, multiple-choice, and multiple-correct, ordering, matching, short-answer, essay, and so forth. These question/answer formats take advantage of all the HTML form elements, including radio buttons, check-boxes, text fields, drop-down lists, list boxes, and text areas. We will build the quizzes from static JSON data files, and using AngularJS’s services, controllers, routes, views, templates, directives, and custom directives.

In the first example, we will use AngularJS’s factory service, controller, partial templates, view, routing, and built-in directive features to read JSON data from a file, and display and validate a basic true-false quiz. In the second example, we will expand our true-false quiz to contain additional types of questions, including multiple-choice and multiple-correct. For the advanced quiz, we will make use of use custom directives and partial view templates. These two new features will allow us to increase the quizzes complexity without substantially increasing the complexity of code we need to write.

Installing and Configuring the Project

This post’s project is available on GitHub. The easiest way to obtain all the source code, is to clone the project with Git. Once you have cloned the project, don’t forget to install the npm and bower packages. All commands are shown below. The minimum requirements for the project, are to have Bower, Grunt, npm, and Git installed.

git clone https://github.com/garystafford/angular-quiz.git
cd angular-quiz
npm install
bower install

Alternately, if you are experienced building JavaScript applications with the scaffolding tool, yo, you can create a new project and recreate the code yourself. To use generator-angular’s code generators, you will need yo installed, in addition to Bower, Grunt, npm, and Git. Since this post’s project is based on the Yeoman’s generator-angular, you can use npm to install Yeoman’s generator-angular. Afterwards, using generator-angular’s available code generators, you can easily reproduce the post’s basic project structure.

npm install -g generator-angular

# Use generator-angular code generators to create project components
# Instructions here: https://github.com/yeoman/generator-angular
mkdir quiz-app && cd $_
yo angular quiz
yo angular:route quizAdvanced
yo angular:factory quizAdvancedFactory
yo angular:directive quizTrueFalseDirective
Using yo with generator-angular to Set-up a New Application

Using yo with generator-angular to Set-up a New Application

Using yo with generator-angular to Create New Components

Using yo with generator-angular to Create New Components

If you used the generator-angular code generator to create the project yourself, using the above instructions, your module will be called ‘quizApp’. The application name, found in the ‘package.json’ and ‘bower.json’ files, will be ‘quiz’. I changed my project’s module and app names to be more descriptive, along with the names of the routes, factories, directives, and other components. They will also vary slightly using the code generators.

Also, if you used the generator-angular code generator to create the project yourself, you may need to install a few additional npm and bower packages, not part of generator-angular project, to reproduce this post’s project, exactly.

Project Structure

The project structure follows the generator-angular format. Most core application files are kept in the ‘app’ folder. This post’s project has added the ‘app/data’ folder, which holds the quiz data, and the ‘app/scripts/partials’, which holds the partial view templates for the custom directives (explained later).

Project View from WebStorm 8

Project View from WebStorm 8

Starting the Project

The project is started using the ‘grunt serve‘ command. Using the grunt server, the project be hosted on ‘localhost’, port 9000, by default. This can be changed to a specific hostname or IP address by editing the ‘Gruntfile.js’ file’s ‘connect‘ task.

Testing the Project

There are some basic tests created using the Karma, Test Runner for JavaScript. These tests are run using the ‘grunt test‘ command. Test are set to run on port 8092, using the PhantomJS web browser. PhantomJS, if you’re not familiar, is a headless WebKit scriptable with a JavaScript API. PhantomJS is ideal for use with Continuous Integration Servers, such as TravisCI. If you do not have PhantomJS installed, and plan to run the tests, change the ‘browser‘ property in the ‘karma.conf.js’ file, located in the project’s root directory. Chrome is a good alternative for local testing. Test results for this GitHub project can be reviewed on TravisCI.

Creating a complete set of unit tests for the advanced quiz proved challenging based on its nested, partial view templates, described in the Advanced Quiz section. I may add a more complete set of unit test in the future.

Basic Quiz

The first quiz is a six-question, basic true-false format form. The user answers all six questions, and then pushes a button to display the results.

Basic Quiz Before User Input

Basic Quiz Before User Input

Basic Quiz With User Input

Basic Quiz With User Input

The basic quiz uses a single controller (quizBasicController.js), single factory service (quizBasicFactory.js), single route (apps.js – ‘/quizBasic’), and a single partial view template (quiz-basic.html), in addition to the main layout (index.html). All these components are part the ‘quizModule’ AngularJS module. I’ve attempted to illustrate these relationships in the diagram, below.

The factory service (quizBasicFactory.js) uses $resource, a service in AngularJS’s ngResource module, to load the contents of a local JSON-format file (quiz-basic.json).

angular.module('quizModule')
 .factory('quizBasicFactory', function ($resource) {
 return $resource('./data/quiz-basic.json');
 });
{
  "name":      "Basic Quiz Example",
  "questions": [
    {
      "_id":      1,
      "question": "AngularJS is a declarative programming language.",
      "answer":   true
    },
    {
      "_id":      2,
      "question": "The acronym 'SPA' stands for Single-Page Application.",
      "answer":   true
    },
    {
      "_id":      3,
      "question": "AngularJS is written in C++.",
      "answer":   false
    }
    ...
  ]
}

The controller (quizBasicController.js), calls the factory service (quizBasicFactory.js), which returns the ‘data’ object.

angular.module('quizModule')
  .controller('QuizBasicController',
  function ($scope, quizBasicFactory) {
    var createResults;
    $scope.title = null; // quiz title
    $scope.quiz = {}; // quiz questions
    $scope.results = []; // user results

    quizBasicFactory.get(function (data) {
      $scope.title = data.name;
      $scope.quiz = data.questions;
      createResults();
    });

    // prepare array of result objects
    createResults = function () {
      var len = $scope.quiz.length;
      for (var i = 0; i < len; i++) {
        $scope.results.push({
          _id:        $scope.quiz[i]._id,
          answer:     $scope.quiz[i].answer,
          userChoice: null,
          correct:    null
        });
      }
    };

    // assign and check user's choice
    $scope.checkUserChoice = function (question, userChoice) {
      // assign the user's choice to userChoice
      $scope.results.userChoice = userChoice;

      // check the user's choice against the answer
      if ($scope.results.answer === userChoice) {
        $scope.results.correct = 'Correct';
      } else {
        $scope.results.correct = 'Incorrect';
      }
    };

    // only show results if all questions are answered
    $scope.checkQuizCompleted = function () {
      var len = $scope.results.length;
      for (var i = 0; i < len; i++) {
        if ($scope.results[i].userChoice === null) {
          return true;
        }
      }
      return false;
    };
  });
The 'data' Object Returned from Factory Service containing  JSON Data

The ‘data’ Object Returned from Factory Service containing JSON Data

Contents of the ‘data’ object are used to populate ‘$scope.quiz[]’, ‘$scope.title’, and ‘$scope.results[]’ properties. The $scope holds the quiz data ($scope.quiz[]), the quiz title ($scope.title), and the results ($scope.results[]). The ‘$scope.checkUserChoice()’ method stores the user’s answer in ‘$scope.results[].answer’ property, and evaluates if the answer is correct ($scope.results[].correct). The ‘$scope.checkQuizCompleted()’ method checks to make sure all questions have been answered before showing the results, when the user clicks the ‘Show Results’ button.

The $scope Containing Quiz, Title, and Results Properties

The $scope Containing Quiz, Title, and Results Properties

AngularJS bootstraps the application. Through AngularJS’s compiling and linking process, the partial view template (quiz-basic.html), shown below, the controller (quizBasicController.js), and the main layout (index.html), form the ‘\quizBasic’ View, which is presented to the user. Blogger, Dag-Inge Aas does a nice job of explaining this process in his post, Understanding template compiling in AngularJS.

<h4 class="title">{{title}}
<br/>

<!--quiz section-->
<form name="quiz">
  <div ng-repeat="question in quiz">
    <strong>{{question._id}}. {{question.question}}</strong>

    <div class="radio">
      <input required
             name="_id{{question._id}}"
             type="radio"
             value="true"
             ng-model="question.userChoice"
             ng-change="$parent.checkUserChoice(question._id, true)"/>
      <label for="_id{{question._id}}">True</label>
      <br/>
      <input required
             name="_id{{question._id}}"
             type="radio"
             ng-value="false"
             ng-model="question.userChoice"
             ng-change="$parent.checkUserChoice(question._id, false)"/>
      <label for="_id{{question._id}}">False</label>
    </div>
  </div>
</form>

<hr/>

<!--results section-->
<div ng-init="showAnswers=true">
  btn-sm"
          ng-click="showAnswers=checkQuizCompleted()">
    Show Results
  </button>
  <br/>
  <br/>

  <div ng-hide="showAnswers">
    <strong>Results</strong>

    <div ng-repeat="result in results">
      {{result._id}}. <span
        ng-class="result.correct == 'Correct' ? 'correct' : 'incorrect'">
        {{result.correct}}
      </span>
    </div>
  </div>

We load all the contents of the JSON data file into $scope and use the ‘ng-repeat‘ directive to iterate over the questions ($scope.quiz[]) and the results ($scope.results[]). Because of this, modifying existing questions and adding new ones is easy. This requires no additional coding, just a change to the JSON data file.

 Advanced Quiz

Using all the same basic building blocks as the basic quiz, with the addition of custom-directives, we can add complexity to our quiz, without a lot of additional coding. This advanced quiz has nine questions, including three true-false format, three multiple-choice format, and three multiple-correct format. As the user answers each questions, they are presented with the results, either ‘Correct’ or ‘Incorrect’.

Advanced Quiz Before User Input

Advanced Quiz Before User Input

Advanced Quiz With User Input

Advanced Quiz With User Input

Similar to the basic quiz, the advanced quiz uses a single controller (quizAdvancedController.js), factory service (quizAdvancedFactory.js), route (apps.js – ‘/quizAdvanced’), partial view template (quiz-advanced.html), and the main layout (index.html). Additionally, the advanced quiz uses a filter, three custom directives, and four partial view templates. The fourth partial view template, ‘quiz-choice-response.html’, is called by the first three partial view templates. It contains common DOM elements. Like the basic quiz, all these components are part the ‘quizModule’ module. I’ve attempted to illustrate these relationships in the diagram, below.

Just like with the basic quiz, the factory service (quizAdvancedFactory.js) uses $resource to load the contents of a local JSON-format file (quiz-advanced.json). This time however, the JSON file contains three types of questions, each with a slightly different schema. The three different question types are shown in the code snippet below. The true-false questions have a boolean value as the answer, the multiple choice questions, an integer as an answer, and the multiple correct questions, an array of integers as an answer.

angular.module('quizModule')
  .factory('quizAdvancedFactory', function ($resource) {
    return $resource('./data/quiz-advanced.json');
  });
{
  "name":      "Advanced Quiz Example",
  "questions": [
    {
      "_id":      1,
      "question": "AngularJS is written completely in JavaScript.",
      "type":     "True-false",
      "answer":   true
    },
    {
      "_id":      4,
      "question": "What does the acronym 'MVC' stand for?",
      "type":     "Multiple choice",
      "choices":  [
        {
          "_id":    1,
          "choice": "Method, Variable, Constant"
        },
        {
          "_id":    2,
          "choice": "Module, View, Constraint"
        },
        {
          "_id":    3,
          "choice": "Model, View, Controller"
        },
        {
          "_id":    4,
          "choice": "None of the above"
        }
      ],
      "answer":   3
    },
    {
      "_id":      7,
      "question": "Which of the following are associated with AngularJS?",
      "type":     "Multiple correct",
      "choices":  [
        {
          "_id":    1,
          "choice": "Controller"
        },
        {
          "_id":    2,
          "choice": "Interface"
        },
        {
          "_id":    3,
          "choice": "Route"
        },
        {
          "_id":    4,
          "choice": "View"
        },
        {
          "_id":    5,
          "choice": "Model"
        },
        {
          "_id":    6,
          "choice": "Generator"
        },
        {
          "_id":    7,
          "choice": "Service"
        },
        {
          "_id":    8,
          "choice": "Node"
        }
      ],
      "answer":   [1, 3, 4, 5, 7]
    }
    ...
  ]
}

The controller (quizAdvancedController.js), calls the factory service (quizAdvancedFactory.js), which returns the ‘data’ object, just like in the basic quiz example.

angular.module('quizModule')
  .controller('QuizAdvancedController',
  function ($scope, quizAdvancedFactory, filterFilter) {
    var createResults;
    $scope.title = null; // quiz title
    $scope.quiz = {}; // quiz questions
    $scope.results = []; // user results

    quizAdvancedFactory.get(function (data) {
      $scope.title = data.name;
      $scope.quiz = data.questions;
      createResults();
    });

    // prepare array of result objects
    createResults = function () {
      var len = $scope.quiz.length;
      for (var i = 0; i < len; i++) {
        $scope.results.push({
          _id:        $scope.quiz[i]._id,
          answer:     $scope.quiz[i].answer,
          userChoice: null,
          correct:    null
        });
      }
    };

    // used for multiple correct type questions
    $scope.checkUserMultiCorrectChoice = function (question, userChoice) {
      // create blank array
      if ($scope.results.userChoice === null) {
        $scope.results.userChoice = [];
      }

      // find choice, if not there the add or if there remove
      var pos = $scope.results.userChoice.indexOf(userChoice);
      if (pos < 0) {
        $scope.results.userChoice.push(userChoice);
      } else {
        $scope.results.userChoice.slice(pos, 1);
      }

      // check the user's choice against the answer
      var answer = JSON.stringify($scope.quiz.answer.sort());
      var choice = JSON.stringify($scope.results.userChoice.sort());

      if (answer === choice) {
        $scope.results.correct = true;
      } else {
        $scope.results.correct = false;
      }
    };

    // used for multiple choice and true-false type questions
    $scope.checkUserChoice = function (question, userChoice) {
      // assign the user's choice to userChoice
      $scope.results.userChoice = userChoice;

      // check the user's choice against the answer
      if ($scope.results.answer === userChoice) {
        $scope.results.correct = true;
      } else {
        $scope.results.correct = false;
      }
    };

    // find a specific question
    $scope.filteredQuestion = function (questionId) {
      return filterFilter($scope.quiz, {_id: questionId});
    };
  });

For true-false and multiple-choice questions, the ‘$scope.checkUserChoice()’ method stores the user’s answer in the ‘$scope.results[].answer’ property. The method also evaluates if the answer is correct, and stores that value in the ‘$scope.results[].correct’ property. The method takes two input parameters, question id and user’s choice.

For multiple correct questions, the ‘$scope.checkUserMultiCorrectChoice()’ method does the same. The difference, for multiple-correct questions, the method stores both the multiple answers and multiple user choices in a pair of arrays, ‘$scope.results[].answer[]’ and ‘$scope.results[].userChoice[]’ object arrays. In addition to storing the user’s choices, the method removes user choices if they are deselected by the user, in the view.

Lastly, the ‘$scope.checkUserMultiCorrectChoice()’ method evaluates the user’s choices array against the correct answers array. In the example below, note the ‘$scope.results[6].answer[]’ array and the ‘$scope.results[6].userChoice[]’ array. They were determined to be equal by the ‘$scope.checkUserMultiCorrectChoice()’, and reflected in the ‘true’ value of the ‘$scope.results[6].correct’ property.

Advanced Quiz Results for Multiple-Correct Question

Advanced Quiz Results for Multiple-Correct Question

Filter

In the ‘quizAdvancedController.js’ controller, note the ‘filterFilter’ object injected into the controller’s main function. At the end of the controller, also note the ‘$scope.filterQuestion(questionId)’ method.

angular.module('quizModule')
  .controller('QuizAdvancedController',
  function ($scope, quizAdvancedFactory, filterFilter) {
    ...
    // find a specific question
    $scope.filteredQuestion = function (questionId) {
      return filterFilter($scope.quiz, {_id: questionId});
    };
  });

The ‘$scope.filterQuestion(questionId)’ method takes a question id as an input parameter, and returns that single question. The ‘$scope.filterQuestion(questionId)’ method actually returns a call to the angular.filter‘s filterFilter. It takes two parameters,  an array containing the entire set of questions (‘$scope.quiz’ array), and a ‘pattern object’ containing the specific ‘id’ to filter on (‘{_id: questionId}’).

The filter method is called by the three question-type partial view templates, for example ‘quiz-multi-choice.html’. For example, the partial view template, ‘quiz-advanced.html’, uses the ‘quiz-multichoice’ element to call the custom directive, ‘quizMultiChoiceDirective.js’, passing it a request for question id 4.

<h4 class="title">{{title}}</h4>
<br/>
<form name="quiz">
  <!--true-false-->
  <quiz-truefalse filter-by="1"></quiz-truefalse>
  <quiz-truefalse filter-by="2"></quiz-truefalse>
  <quiz-truefalse filter-by="3"></quiz-truefalse>

  <!--multi-choice-->
  <quiz-multichoice filter-by="4"></quiz-multichoice>
  <quiz-multichoice filter-by="5"></quiz-multichoice>
  <quiz-multichoice filter-by="6"></quiz-multichoice>

  <!--multi-correct-->
  <quiz-multicorrect filter-by="7"></quiz-multicorrect>
  <quiz-multicorrect filter-by="8"></quiz-multicorrect>
  <quiz-multicorrect filter-by="9"></quiz-multicorrect>
</form>

The custom directive, ‘quizMultiChoiceDirective.js’, loads the partial view template, ‘quiz-multi-choice.html’, using the ‘templateUrl’ argument. The ‘templateUrl’ argument uses ajax to load the template. The template, ‘quiz-multi-choice.html’, uses the ‘ng-repeat‘ directive to populate its section of the advanced quiz with question id 4 (div ng-repeat="question in $parent.filteredQuestion(filterBy)). It does so by calling filteredQuestion(4), in the ‘quizAdvancedController.js’ controller.

<div ng-repeat="question in $parent.filteredQuestion(filterBy)">
  <strong>{{question._id}}. {{question.question}}</strong>
  <div class="radio" ng-repeat="choice in question.choices">
    <input
        name="_id{{question._id}}"
        type="radio"
        value="{{choice._id}}"
        ng-model="question.userChoice"
        ng-change="$parent.$parent.$parent.checkUserChoice(question._id, choice._id)"/>
    <label for="_id{{question._id}}">{{choice.choice}}</label>
  </div>
  <div ng-include src="'/scripts/partials/quiz-choice-response.html'"></div>
</div>
<br/>

The ‘quiz-multi-choice.html’ template also loads the contents of the ‘choice-response.html’ template. This template contains DOM elements, common to all three question-type templates.

<div ng-if="$parent.$parent.$parent.results.correct"
     class="result correct">
  <span class="glyphicon glyphicon-thumbs-up"></span>
  Correct!
</div>
<!--specify 'false' because not true (!) would include null (blank)-->
<div ng-if="$parent.$parent.$parent.results.correct === false"
     class="result incorrect">
  <span class="glyphicon glyphicon-thumbs-down"></span>
  Incorrect
</div>

I have attempted to illustrate the filter in the diagram, below. I intentionally left out a few non-essential components to simplify the diagram, such as the main layout, config, route, service, other custom directives, and the JSON data file.

Using these techniques, we can easily extend the quiz, adding new answer types, such as ordering, matching, short-answer, and so forth.

Managing Scope

Being familiar with AngularJS, you should understand how scope works. You should know there is more than one scope, and that scope is normally inherited from the parent scope. Directives such as ng-repeat, ng-switch, ng-view, and ng-include, all create their own child scopes. Said better by AngularJS’s team, ‘in AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { … } — this creates an isolate scope that does not prototypically inherit.‘ We use a number of directives. We also use ‘scope:’ within our custom directives for the advanced quiz example, which breaks the chain of inheritance.

In some of the code examples in this post, you will notice the use of ‘$parent‘, ‘$parent.$parent‘, or even ‘$parent.$parent.$parent‘, instead of simply ‘$scope‘. Sometimes, it necessary to reach outside the current scope, to a parent’s scope (‘$parent‘), or that parent’s parent’s scope (‘$parent.$parent‘). A simple example of this, in the partial view template, ‘quiz-multi-choice.htm’, we call ‘$parent.filteredQuestion(filterBy)‘. The ‘filteredQuestion(filterBy)’ method we need to use is in the parent scope of the template’s scope, so we call ‘$parent’ instead of ‘$scope’.

So how can you determine which scope contains the method or properties you are seeking? Batarang, the AngularJS WebInspector Extension for Chrome. Batarang adds an additional ‘AngularJS’ tab to Developer tools for Chrome. Previously, we were using the example of question id 4 with the AngularJS’s filter. Using the Batarang, below, we can see the question id 4 in the final View. Each question returned using the filter is contained within its own separate scope.

Question #4 in Batarang Models Tab

Question #4 in Batarang Models Tab

This example also shows how complex working with AngularJS’s scope(s) can be. Starting with a particular scope, using Batarang, you can visually move up (parent scope) or down (child scope) within the scope hierarchy. The contents of each scope, the Model, is displayed on the right. Batarang also offers several other feature, seen below, including AngularJS application performance and dependency visualization.

Links

Quiz Question Types (presentation)

Understanding Service Types (article)

Understanding Scopes (article)

Build custom directives with AngularJS (article)

Google I/O 2012 – Better Web App Development Through Tooling (YouTube video)

, , , , , , , , , , ,

2 Comments