Application development: front-end solution with JavaScript

In the previous article we developed the back-end solution for our books application. This article will continue where we stopped.

We’ll develop a front-end solution that can be used with any back-end (Web, mobiles…). You can build on top of the back-end from the previous article (Java, Jersey, Grizzly…) or on top of any other as long as supported services are the same.

The goal of the application is to be able to administer books. In particular, we’ll develop the front-end solution for that application that should be able to:

  • Add new book
  • Update existing book
  • Delete an existing book
  • List all books
  • Display details of the selected book

Disclaimer: This article does not provide detailed tutorial nor instructions how to use frameworks listed below. It is a step by step guide how to develop an sample application with different frameworks and technologies combined. If the same steps as described below are followed, the end result will be a fully developed front-end application

All the code will be done with the TDD approach. If this is your first time working with TDD, tests that we will be writing might look like a waste or overkill. I urge you to give TDD a try. After a bit of practice, most people find TDD indispensable as a way to design their code as well as a safety net. On a unit level, we have all our code always tested and working as we expect. Try to follow the tests. Pick the first one for the specified spec file, run it and observe that it fails, implement just enough to make the test pass, run all tests and observe that they all pass, refactor the code, repeat.

We will be using WebStorm as our favorite IDE.

Completed code can be found in the TechnologyConversationsBooks GIT repository.

The following frameworks and libraries are selected for the front-end of this application:

  • AngularJS: AngularJS is an open-source JavaScript framework, maintained by Google, that assists with running single-page applications. Its goal is to augment web-based applications with model–view–controller (MVC) capability, in an effort to make both development and testing easier.
  • Bootstrap CSS: Global CSS settings, fundamental HTML elements styled and enhanced with extensible classes, and an advanced grid system.
  • JQuery: jQuery is a multi-browser JavaScript library designed to simplify the client-side scripting of HTML.
  • Jasmine: Jasmine is a behavior-driven development framework for testing JavaScript code.

Overview

We will start by setting up Jasmine. Once we’re all set, we’ll create the first specification and move to the implementation in JavaScript. After all specs and JavaScript implementation is done, all that is left is to write the HTML. Once done, we’ll have a fully functional front-end that, when combined with the back-end from the previous article, acts as a fully functional application.

Jasmine Specs

jasmine-horizontalJasmine is very easy to set up. SpecRunner.html contains all the required libraries together with the spec file booksSpec.js. Since our application is relatively small, we’ll put all our specs into one file (booksSpec.js). To run specs, open the SpecRunner.html in your favorite browser.

books.js

AngularJS-largeOur JavaScript should have methods to:

  • List books
  • Create new book
  • Save book
  • Open an existing book
  • Delete an existing book

It will also contain several helper methods like CSS classes, validation patterns, revert operation, etc. Both JavaScript and HTML will be done with the help of AngularJS.

Let us start with our first spec. Remember, we’re writing the code in TDD style with Jasmine specs. Write specs in booksSpec.js, run them from SpecRunner.html and confirm that the last one fails, do the implementation in books.js, run specs again and confirm that they all pass, repeat.

[booksSpec.js]

describe('listBooks method', function() {
  it('should fetch the list of books from the server', function() {
    var books = [listBook1, listBook2];
    // We expect our code to make an GET request to '/api/v1/items'
    httpBackend.expectGET('/api/v1/items').respond(books);
    // Call listBooks method
    scope.listBooks();
    // Flush http responses
    httpBackend.flush();
    // Confirm that book variable is set
    expect(scope.books).toEqual(books);
  });
});

This spec depends on the setup (for example injection of $httpBackend). Please take a look at the final version of the source code booksSpec.js for more info.

Implementation of this spec in JavaScript with AngularJS is straightforward:

[books.js]

angular.module('booksModule', [])
  .controller('booksCtrl', function ($scope, $http) {
    $scope.listBooks = function() {
      // Send HTTP GET request to /api/v1/items
      $http.get('/api/v1/items').then(function(response) {
        // Put the response to the books variable within the scope.
        $scope.books = response.data;
      });
    };
  });

With this method, we have books JSON from the server stored in the scope variable books. Later on we’ll use this data in the HTML.

We will keep the selected book in the variable book. In order to create a new book, there should be a method that will reset data in the book variable. Same applies to the originalBook variable that will be used later on to revert changes.

[booksSpec.js]

describe('newBook method', function() {
  it('should set book to {}', function() {
    scope.newBook();
    expect(scope.book).toEqual({});
  });
  it('store a copy in originalBook', function() {
    expect(scope.originalBook).toEqual({});
  });
});

Implementation would be:

[books.js]

$scope.newBook = function() {
  $scope.book = {};
  $scope.originalBook = angular.copy($scope.book);
};

Next, we should be able to save the book. Every time a book is saved, books variable should be refreshed and book should be reset.

[booksSpec.js]

describe('saveBook method', function() {
  var books;
  beforeEach(function() {
    books = [listBook1, listBook2, listBook3];
    scope.book = listBook3;
    httpBackend.expectPUT('/api/v1/items').respond();
    httpBackend.expectGET('/api/v1/items').respond(books);
    scope.saveBook();
    httpBackend.flush();
  });
  it('should request PUT /api/v1/items', function() {
    // Do nothing. Code was moved to beforeEach
  });
  it('should fetch the new list of books from the server', function() {
    expect(scope.books).toEqual(books);
  });
  it('should reset book', function() {
    expect(scope.book).toEqual({});
  });
});

[books.js]

$scope.saveBook = function() {
  $http.put('/api/v1/items', $scope.book).then(function() {
    $scope.listBooks();
    $scope.newBook();
  });
};

Similar specs and implementations should be done for open, delete and revert operations. Please consult the final versions of booksSpec.js and books.js for details.

Next are methods that return CSS styles. Method cssClass should be used on form elements to display them in green or red depending on whether values are valid or not. As stated earlier, we’re using Bootstrap stylesheet.

[booksSpec.js]

describe('cssClass method', function() {
  it('should return has-error true and has-success false when ngModelController is invalid', function() {
    element.$invalid = true;
    element.$valid = false;
    expect(scope.cssClass(element)).toEqual({'has-error': true, 'has-success': false});
  });
  it('should return has-error false and has-success true when ngModelController is valid and dirty', function() {
    element.$invalid = false;
    element.$valid = true;
    expect(scope.cssClass(element)).toEqual({'has-error': false, 'has-success': true});
  });
});

In AngularJS, each HTML element can be a controller with different states. In this case we’re interested in valid and invalid. Implementation would be following.

[books.js]

$scope.cssClass = function(ngModelController) {
  return {
    'has-error': ngModelController.$invalid,
    'has-success': ngModelController.$valid
  };
};

Similar spec and implementation should be done for the cssClassButton, isValid, canRevertBook and canDeleteBook methods. We’ll use them to display styles and enable/disable elements. All of those methods are similar to the cssClass. Please consult the final versions of booksSpec.js and books.js for details.

Next, we should create a pattern for the price field. It should be a number, followed by dot and two more digits (i.e. 1223.45). Jasmine provides toMatch that can be used to validate regular expressions.

[booksSpec.js]

describe('pricePattern method', function() {
  it('should allow any number of digits followed with dot and two digits (i.e. 1223.45)', function() {
    expect('123.45').toMatch(scope.pricePattern());
    expect('123.').not.toMatch(scope.pricePattern());
    expect('123').not.toMatch(scope.pricePattern());
    expect('.45').not.toMatch(scope.pricePattern());
    expect('.').not.toMatch(scope.pricePattern());
  });
});

Implementation is an regex expression.

[books.js]

$scope.pricePattern = function() {
  return (/^[d]+.dd$/);
};

We’re almost done with JavaScripts. Finally, we should make sure that the booksCtrl controller sets books and book variables when loaded.

[booksSpec.js]

it('should call listBooks method', function() {
  expect(scope.books).toEqual([listBook1]);
});
it('should call newBook method', function() {
  expect(scope.book).toEqual({});
});

Implementation is pretty straight forward.

[books.js]

$scope.listBooks();
$scope.newBook();

We’re finished with JavaScripts. Final version of the specs can be found in the booksSpec.js and the implementation in the books.js.

All that is left now is to create the index.html that will harvest our JavaScripts.

index.html

First thing is to include required JavaScript libraries and specify AngularJS app and controller.

[index.html]

<body ng-app="booksModule">
  <div class="container-fluid" ng-controller="booksCtrl">
...

Next, we should create a link to create a new book. We already created a JavaScript method newBook. Annotation ng-click can be used to execute an method on a click event.

[index.html]

<a href="#" ng-click="newBook()">New Book</a>

Next, we should list all books. Annotation ng-repeat can be used to repeat an element for each item in a collection. As with the newBook, ng-click should be used to call the openBook method. To display some value in read-only mode, value needs it to be surrounded with {{ and }}.

[index.html]

<tbody ng-repeat="book in books">
<tr><td>
  <a href="#" ng-click="openBook(book.id)">{{book.title}}</a>
</td></tr>
</tbody>

In order to create new or update an existing book, we’ll need input elements. In case of the price, it should:

  • Change color to green or red depending on the status: ng-class annotation with the JavaScript method cssClass.
  • Have bidirectional binding between book variable and the HTML input element: ng-model annotation.
  • Have validation pattern that will allow only specific format: ng-pattern annotation with the JavaScript method pricePattern.
  • Display help text if specified value is incorrect: ng-show annotation.

[index.html]

<div class="form-group" ng-class="cssClass(bookForm.price)">
  <label for="price">Price</label>
  <input id="price" name="price" class="form-control" type="text" ng-model="book.price" placeholder="Book Price" required="true" ng-pattern="pricePattern()">
</div>
<div class="help-block" ng-show="bookForm.price.$error.pattern">
  Price must be any number of digits followed with dot and two digits (i.e. 1223.45)
</div>

Similar code should be used for all book attributes: ID, title, author and image URL.

Finally, all that is left is to add save, revert and delete buttons. They should:

  • Appear with certain style depending on the form status: ng-class attribute with the JavaScript method cssClassButton.
  • Be enabled or disabled: ng-disabled attribute with the JavaScript methods isValid, canRevertBook and canDeleteBook.
  • Execute specified JavaScript method when clicked: ng-click annotation with the JavaScript methods saveBook, revertBook and deleteBook.

[index.html]

<button class="btn btn-primary" ng-class="cssClassButton(bookForm)" ng-disabled="!isValid(bookForm)" ng-click="saveBook()" type="button">Save</button>
<button class="btn btn-primary" ng-disabled="!canRevertBook()" ng-click="revertBook()" type="button">Revert</button>
<button class="btn btn-primary" ng-disabled="!canDeleteBook()" ng-click="deleteBook()" type="button">Delete</button>

That’s it. We are finished with the front-end part of the application. Final version of the HTML can be found in the index.html.

To try it out, start the server that we created in the previous article and open http://localhost:8080/page in your favorite browser.

What Next?

Right now we have both back-end and front-end application finished. Actually, there is a lot of work left undone. Security should be increased with server-side validation, books pagination is not done in the front-end, $http failures are not managed, delete action could require additional confirmation, etc. I’ll leave those and other improvements up to you.

Leave a Reply