3. Encode our Web Application with Unidirectional Functional Association

In previous chapters we were only taking care of the Book model. During implementing more models we should consider on reorganizing our folder's structure, such as:

Figure 9.3. The Structure of Public Library Web-application with Book and Publisher

The Structure of Public Library Web-application with Book and Publisher


publicLibrary
|-- ...
|-- js
|   |-- ...
|   |-- controllers.js
|   `-- controllersPackage
|       |-- BooksController.js
|       `-- PublishersController.js
`-- partials
    |-- main.html
    |-- books
    `-- publishers

Currently our logical Angualr model of Book:

$scope.book = {
  "isbn": "",
  "title": "",
  "year": 0,
  "publisher": null
};

And the model of Publisher:

$scope.publisher = {
  "name": "",
  "address": ""
}

Although the class Publisher is now a new model in our application, its structure and code are almost same as the class Book which has been discussed a lot in Chepter 2, so we will take more focus on the important changes of the class Book.

3.1. List Objects Use Case

In the view of showAllBooks.html, we should show all the information for each book including its publisher's name:

<div ng-init="getAllBooks()">
  <h1>List all books</h1>
  <table>
    <thead><tr>
      <th>ISBN</th><th>Title</th><th>Year</th><th>Publisher</th>
    </tr></thead>
    <tbody><tr ng-repeat="book in books">
      <td>{{book.isbn}}</td><td>{{book.title}}</td>
      <td>{{book.year}}</td><td>{{book.publisher.name}}</td>
    </tr></tbody>
  </table>
</div>

book.publisher.name is a property of the publisher object corresponding to this book. We have known that in Parse, a Pointer is poiting to a certain publisher object, so we should get the related publisher in the Controller:

$scope.getAllBooks = function () {
  $http({
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Book',
    params: { include: 'publisher'}
  })
    .success( function ( data) {
      $scope.books = data.results;
    })
    .error( function ( data) {
      console.log( data);
      alert("See the error message in console.");
    });
};

3.2. Create Object Use Case

Except the common input fields isbn, title and year, we add here a HTML element select for selecting an associated object from a list of all existing instances of publisher class, which we name it publishers:

<div ng-init="getReadyToAddBook()">
  <h1>Create a new book record</h1>
  <form name="bookInfo" novalidate="novalidate" ng-submit="addBook()">
    ...
    <div>
      <label>Publisher
        <select name="publisher" ng-model="book.publisher"
                ng-options="publisherOption.name for
                            publisherOption in publishers">
          <option value="">...</option>
        </select>
      </label>
    </div>
    ...
  </form>
</div>

For getting these instances we announce a retrieving function getReadyToAddBook() using ngInit in the first line of this view and implement it in the js/controllersPackage/BooksController.js:

$scope.getReadyToAddBook = function () {
  $scope.book = {
    isbn: "",
    title: "",
    year: 0,
    publisher: null
  };
  // publishers and authors for selection
  $scope.publishers = [];
  $http( {
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Publisher'
  })
    .success( function ( data) {
      $scope.publishers = data.results;
    })
    .error( function ( data) {
      console.log( data);
    });
};

After user filled all information of a new book and clicked ths Save button, the function addBook() in the js/controllersPackage/BooksController.js will be invoked:

$scope.addBook = function() {
  if ( $scope.bookInfo.$valid) {
    $http( {
      method: 'POST',
      url: 'https://api.parse.com/1/classes/Book',
      data: {
        isbn: $scope.book.isbn,
        title: $scope.book.title,
        year: $scope.book.year
      }
    })
      .success( function ( data) {
        var bookObjectId = data.objectId;
        // if book.publisher is defined,
        // add pointer to this publisher
        if ( $scope.book.publisher) {
          $http( {
            method: 'PUT',
            url: 'https://api.parse.com/1/classes/Book/' +
                 bookObjectId,
            data: {
              publisher: {
                __type: 'Pointer',
                className: 'Publisher',
                objectId: $scope.book.publisher.objectId
              }
            }
          })
            .error( function ( data) {
              console.log( data);
            });
        }
      })
      .error( function ( data) {
        console.log( data);
        alert("ERROR: See the error message in console.");
      });
  } else {
    console.log("Form is invalid!");
  }
};

Notice that the first request method is POST for upload a new book and the second request method is PUT for building the relation between publisher object and book object.

3.3. Update Object Use Case

Adding Publisher item for update use case:

<div ng-init="getAllBooks()">
  <h1>Update a book record</h1>
  <form name="bookInfo" novalidate="novalidate" ng-submit="updateBook()">
    ...
      <label>Publisher
        <select name="publisher" ng-model="book.publisher"
                ng-options="publisherOption.name for
                            publisherOption in publishers">
          <option value="">...</option>
        </select>
      </label>
    ...
  </form>
</div>

We should modify the previous function selectBook() in order to load all existing publisher objects. If the selected book has already a publisher, we should retrieve it in the list of publisher object options and set this publisher option to be selected:

var tempBook = {
  isbn: "",
  title: "",
  year: 0,
  publisher: null
};

$scope.selectBook = function () {
  $scope.isSelected = true;
  tempBook = angular.copy( $scope.book);
  $scope.$watchCollection( 'book', function (newBook) {
    if ( angular.equals( newBook, tempBook)) {
      $scope.bookInfo.$setPristine();
    }
  });
  // get all publishers
  $scope.publishers = [];
  $http( {
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Publisher'
  })
    .success( function ( data) {
      $scope.publishers =  data.results;
      if ( $scope.book.publisher) {
        // if has a publisher, set default to select this publisher
        var idx = 0;
        while ( $scope.publishers[idx].objectId !=
                $scope.book.publisher.objectId) {
          idx++;
        }
        $scope.book.publisher = $scope.publishers[idx];
      }
    })
    .error( function ( data) { console.log( data);});
};

Considering the redundancy of the functions between getReadyToAddBook() and selectBook(), it's necessary to compact them together into one function named prepareBook() (Remember to change the names of these functions for the Create Ojbect and Update Object use cases):

$scope.prepareBook = function () {
  if ( $scope.book) {
    $scope.isSelected = true;
    tempBook = angular.copy( $scope.book);
    // watcher whether the book has been changed or not
    $scope.$watchCollection( 'book', function (newBook) {
      if ( angular.equals( newBook, tempBook)) {
        $scope.bookInfo.$setPristine();
      }
    });
  } else {
    $scope.book = angular.copy( tempBook);
  }
  // get publishers
  $scope.publishers = [];
  $http( {
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Publisher'
  })
    .success( function ( data) {
      $scope.publishers =  data.results;
      if ( $scope.book.publisher) {
        // if has a publisher, set default to select this publisher
        var idx = 0;
        while ( $scope.publishers[idx].objectId !=
                $scope.book.publisher.objectId) {
          idx++;
        }
        $scope.book.publisher = $scope.publishers[idx];
      }
    })
    .error( function ( data) { console.log( data);});
};

During updating a modified book record, we should consider that whether the Pointer has been changed or not, so we need separate the PUT request, that one for updating normal book record and another for updating the relation:

$scope.updateBook = function () {
  if ( $scope.bookInfo.$valid &&
       !angular.equals( $scope.book, tempBook)) {
    $http( {
      method: 'PUT',
      url: 'https://api.parse.com/1/classes/Book/' +
           $scope.book.objectId,
      data: {
        title: $scope.book.title,
        year: $scope.book.year
      }
    })
      .success( function () {
        // Considering whether the publisher of this book
        // has been changed or not
        if ( !angular.equals( $scope.book.publisher, tempBook.publisher)) {
          if ( $scope.book.publisher == null) {
            $http( {
              method: 'PUT',
              url: 'https://api.parse.com/1/classes/Book/' +
                   $scope.book.objectId,
              data: { publisher: { __op: 'Delete'}}
            })
              .error( function ( data) { console.log( data);});
          } else {
            $http( {
              method: 'PUT',
              url: 'https://api.parse.com/1/classes/Book/' +
                   $scope.book.objectId,
              data: {
                publisher: {
                  __type: 'Pointer',
                  className: 'Publisher',
                  objectId: $scope.book.publisher.objectId
                }
              }
            })
              .error( function ( data) { console.log( data);});
          }
        }
        $location.path('/books/manageBooks');
      })
      .error( function ( data) {
        console.log( data);
        alert("ERROR: See the information in console.");
      });
  } else {
    console.log("Form is invalid or you didn't change the information!");
  }
};

3.4. Delete Object Use Case

Compared to the single-class app discussed in chapter 2, we do not need to modify the function of destroyBook() but only need to insert a publisher item for this view, if we want to show the complete information of a book:

<div ng-init="getAllBooks()">
  <h1>Delete a book record</h1>
  <form name="bookInfo">
    ...
    <div>
      <label>Publisher
        <output name="publisher" ng-bind="book.publisher.name" />
      </label>
    </div>
  </form>
  ...
</div>