3. Encode our Web Application with Unidirectional Non-Functional Association

First creating a folder and some files for our new class Author:

publicLibrary
|-- ...
|-- js
|   |-- ...
|   `-- controllersPackage
|       |-- ...
|       `-- AuthorsController.js
`-- partials
    |-- ...
    `-- authors

Currently the logical Angualr model Book will be:

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

3.1. Special processing for Date

The logical model Author in our current application will be:

$scope.author = {
  "authorId": 0,
  "name": "",
  "birthDate": Date,
  "deathDate": Date
}

For showing the information of birthDate and deathDate, we would like to use an Angular filter date for formatting it to a string:

{{author.birthDate | date: "yyyy-MM-dd"}}
{{author.deathDate | date: "yyyy-MM-dd"}}

Angular also provides a directive for the input with date, which we can use in our application:

<label>Date of Birth
  <input type="date" name="birthDate" ng-model="author.birthDate"
         placeholder="yyyy-MM-dd" ng-required="true" />
</label>
<label>Date of Death
  <input type="date" name="deathDate" ng-model="author.deathDate"
         ng-min="author.birthDate" placeholder="yyyy-MM-dd" />
</label>

Please notice that the properties birthDate and deathDate, which are in the type of Date, will be casted to the type of String by Parse. therefore after getting the author records, we should also make a conversion for them from String to Date. Besides, if the deathDate is empty, its type will be casted to the type of null:

$scope.getAllAuthors = function () {
  $http( {
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Author'
  })
    .success( function ( data) {
      $scope.authors = data.results;
      // format string date to real date
      $scope.authors.forEach( function( a) {
        a.birthDate = new Date( a.birthDate);
        if ( a.deathDate) {
          a.deathDate = new Date( a.deathDate);
        }
      });
    })
    .error( function ( data) {
      console.log( data);
      alert("ERROR: See the information in console.");
    });
};

The structure of Author and its rest of code are almost same as the class Book in Part 2 (Validation Tutorial). Now take our focus on the important changes of the class Book.

3.2. List Objects Use Case

Because of adding a new property authors in the class Book, by showing the information of book records we should also insert the corresponding author's name:

<h1>List all books</h1>
<table>
  <thead><tr>
    <th>ISBN</th><th>Title</th><th>Year</th>
    <th>Authors</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><ul>
      <li ng-repeat="author in book.authors">{{author.name}}</li>
    </ul></td>
    <td>{{book.publisher.name}}</td>
  </tr></tbody>
</table>

For getting the related authors for each book using Parse Relation:

$scope.getAllBooks = function () {
  $http({
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Book',
    params: { include: 'publisher'}
  })
    .success( function ( data) {
      $scope.books = data.results;
      $scope.books.forEach( function ( b) {
        // if this book has authors,
        // get related authors using Parse Relation
        if ( b.authors) {
          $http( {
            method: 'GET',
            url: 'https://api.parse.com/1/classes/Author',
            params: {
              where: {
                $relatedTo: {
                  object: {
                    __type: 'Pointer',
                    className: 'Book',
                    objectId: b.objectId
                  },
                  key: 'authors'
                }
              }
            }
          })
            .success( function ( data) {
              b.authors = angular.copy( data.results);
            })
            .error( function ( data) {
              console.log(data);
            });
        }
      });
    })
    .error( function ( data) {
      console.log( data);
      alert("ERROR: See the information in console.");
    });
};

3.3. Create Object Use Case

One book could have no or several authors. Dealing with this One-to-Many relation we use a combination of element ul and element select and two functions addAuthorOption() and removeAuthorOption():

<div ng-init="prepareBook()">
  <h1>Create a new book record</h1>
  <form name="bookInfo" novalidate="novalidate" ng-submit="addBook()">
    ...
    <div>
      <label>Authors
        <div>
          <ul>
            <li ng-repeat="author in book.authors">
              <span>{{author.name}}</span>
              <button type="button"
                      ng-click="removeAuthorOption(author)">
                x
              </button>
            </li>
          </ul>
        </div>
        <div>
          <select ng-model="authorOption"
                  ng-options="authorOption.name for
                              authorOption in authors">
            <option value="">...</option>
          </select>
          <button type="button" ng-click="addAuthorOption()">
            add
          </button>
        </div>
      </label>
    </div>
    ...
  </form>
</div>

We had discussed the function prepareBook() in previous chapter for Publisher. This function will be extended for Author, we will show its code in next section Update Object use case. Now we only need to kown that it will get all author records.

We have two lists that in this case:

  • book.authors gets ready for saving the authors who authored this book;

  • authors saves all author objects.

The function addAuthorOption is combined with an click event for the button add, will save the current selected author into book.authors and delete this author from authors:

$scope.addAuthorOption = function () {
  if ( $scope.authorOption) {
    $scope.book.authors.push( $scope.authorOption);
    var indx = $scope.authors.indexOf( $scope.authorOption);
    $scope.authors.splice( indx, 1);
    $scope.authorOption = null;
  } else {
    console.log("please choose a author.");
  }
};

The function removeAuthorOption is combined with an click event for the button x, will delete one corresponded author object, which is the argument of this function, from book.authors and save it into authors:

$scope.removeAuthorOption = function( author) {
  $scope.authors.push( author);
  var indx = $scope.book.authors.indexOf( author);
  $scope.book.authors.splice( indx, 1);
  $scope.bookInfo.$setDirty();
};

By uploading this new book record, we should click the button of Save, which invokes the function addBook():

$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 ( $scope.book.authors.length > 0) {
          $scope.book.authors.forEach( function ( a) {
            $http( {
              method: 'PUT',
              url: 'https://api.parse.com/1/classes/Book/' +
                   bookObjectId,
              data: {
                authors: {
                  __op: 'AddRelation',
                  objects: [ {
                    __type: 'Pointer',
                    className: 'Author',
                    objectId: a.objectId
                  }]
                }
              }
            })
              .error( function ( data) { console.log( data);});
          });
        }
      })
      .error( function ( data) {
        console.log( data);
        alert("ERROR: See the information in console.");
      });
  } else {
    console.log("Form is invalid!");
  }
};

3.4. Update Object Use Case

It's some as the Create Object use case that we use two elements and two functions for the multi-valued reference property authors:

<h1>Update a book record</h1>
<form name="bookInfo" novalidate="novalidate" ng-submit="updateBook()">
  <div ng-hide="isSelected">
    <label>Select book
      <select ng-model="book" ng-change="prepareBook()"
              ng-options="book.title for book in books">
        <option value="">...</option>
      </select>
    </label>
  </div>
  <div ng-show="isSelected">
    ...
    <div>
      <label>Authors
        <div>
          <ul>
            <li ng-repeat="author in book.authors">
              <span>{{author.name}}</span>
              <button type="button"
                      ng-click="removeAuthorOption(author)">
                x
              </button>
            </li>
          </ul>
        </div>
        <div>
          <select ng-model="authorOption"
                  ng-options="authorOption.name for
                              authorOption in authors">
            <option value="">...</option>
          </select>
          <button type="button" ng-click="addAuthorOption()">
            add
          </button>
        </div>
      </label>
    </div>
    ...
  </div>
</form>

We have here also two lists for author objects, but:

  • book.authors saves the authors who authored this book;

  • authors saves the rest author objects except those who authored this book.

By selecting a book the function prepareBook() will be invoked, it just used for predefining the two lists:

var tempBook = {
  ...
  "authors": []
};

$scope.prepareBook = function () {
  ...
  // get authors
  $scope.authors = [];  
  $http( {
    method: 'GET',
    url: 'https://api.parse.com/1/classes/Author'
  })
    .success( function ( data) {
      $scope.authors = data.results;
      if ( $scope.book.authors.length > 0) {
        $scope.book.authors.forEach( function ( a) {
          // if has authors, remove these authors
          // from authors list
          $scope.authors.forEach( function ( ao) {
            if ( ao.authorId === a.authorId)
              var indx = $scope.authors.indexOf( ao);
              $scope.authors.splice( indx, 1);
          });
        });
      }
    })
    .error( function ( data) { console.log( data);});
};

Taking focus on the changing of book's authors, we should consider:

  • if an existed author has been removed from a book, send a PUT request with operation RemoveRelation;

  • if a new author has been inserted to a book, send a PUT request with operation AddRelation;

  • if an author hasn't been removed or inserted, do not need to send any request.

The common method will be:

  1. create two arrays: removeRelatedAuthors, stores which authors should be removed, and addRelatedAuthors, stores all new added authors.

  2. compare between the old and new author's lists and save the changes to the corresponding array.

  3. PUT all changes to Parse.com cloud storage.

But normally there is little book which has more than three authors, so by updating the book record, we remove all old related authors, then add all new related authors, when the authors of this book has been changed and not empty:

$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 () {
        if ( !angular.equals( $scope.book.authors, tempBook.authors)) {
          // if old author lists of this book is not empty,
          // remove all Parse Relations
          if ( tempBook.authors.length > 0) {
            tempBook.authors.forEach( function ( a) {
              // remove relation from book to author
              $http({
                method: 'PUT',
                url: 'https://api.parse.com/1/classes/Book/' +
                     tempBook.objectId,
                data: {
                  authors: {
                    __op: 'RemoveRelation',
                    objects: [{
                      __type: 'Pointer',
                      className: 'Author',
                      objectId: a.objectId
                    }]
                  }
                }
              })
                .error( function ( data) { console.log( data);});
            });
          }
          // if updated authors of this book is not empty,
          // add Parse relations
          $timeout( function () {
            if ( $scope.book.authors.length > 0) {
              // add all new authors
              $scope.book.authors.forEach( function ( a) {
                // add relation from book to author
                $http({
                  method: 'PUT',
                  url: 'https://api.parse.com/1/classes/Book/' +
                       $scope.book.objectId,
                  data: {
                    authors: {
                      __op: 'AddRelation',
                      objects: [{
                        __type: 'Pointer',
                        className: 'Author',
                        objectId: a.objectId
                      }]
                    }
                  }
                })
                  .error( function ( data) {
                    console.log( data);
                  });
              });
            }
          }, 100);
        }
      })
      .error( function ( data) {
        console.log( data);
        alert("ERROR: See the error message in console.");
      });
  } else {
    console.log("Form is invalid or you didn't change the information!");
  }
};

3.5. Delete Object Use Case

If we also want to show the author's list of a selected book in this use case, we only need to create a list with ng-repeat:

<div ng-init="getAllBooks()">
  <h1>Delete a book record</h1>
  <form name="bookInfo">
    ...
    <div>
      <label>Author
        <ul>
          <li ng-repeat="author in book.authors">
            <output name="author" ng-bind="author.name" />
          </li>
        </ul>
      </label>
    </div>
    ...
  </form>
</div>