Creating An AngularJS Application With Sequelize - Part 2

This is the second part of a three-part series on building an angularJS application with Sequelize broken down as follows.

Understanding Our Frontend Structure

Just like we did in the server directory , we will need to set up a few things for our frontend.

Our images, stylesheets, scripts and jade files will be in the app directory. We will then add gulp tasks to compile and minify the files to a public directory which will serve our application on the browser.

Table of Contents

    ├── index.js
    ├── gulpfile.js
    ├── package.json
    ├── bower.json
    ├── .bowerrc
    ├── .sequelizerc
    ├── app/
        ├── images/
        ├── js/
            ├── controllers/
            ├── directives/
            ├── filters/
            ├── services/
        ├── styles/
        ├── views/
        ├── app.js
        ├── index.jade
    ├── public
        ├── images/
        ├── js/
            ├── index.js
        ├── css/
            ├── app.css
        ├── views/
        ├── index.html
    ├── server
        ├── ...

    Refactor Routes To Routes File

    Currently, we have clogged up our /index.js with routes to our API. Let's refactor the routes into a routes file.

    server/routes/index.js

    var authors = require('../controllers/authors'),
      books = require('../controllers/books');
    
    module.exports = function (router) {
      router.get('/authors', authors.index);
      router.get('/authors/:id', authors.show);
      router.post('/authors', authors.create);
      router.put('/authors', authors.update);
      router.delete('/authors/:id', authors.delete);
    
      router.get('/books', books.index);
      router.get('/books/:id', books.show);
      router.post('/books', books.create);
      router.put('/books/:id', books.update);
      router.delete('/books/:id', books.delete);
    
      return router
    };

    Our index.js file is now much cleaner and does not handle any routes logic.

    index.js

    var express = require('express'),
      routes = require('./server/routes')
      bodyParser = require('body-parser');
    
    var app = express();
    
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(express.static(process.cwd() + '/public'));
    
    //App routes
    app.use(routes(express.Router()));
    
    app.get('/*', function (req, res) {
      res.sendFile('index.html', {
        root: './public'
      });
    });
    
    app.set('port', process.env.PORT || 8000);
    app.listen(app.get('port'), function () {
      console.log("Magic happens on port", app.get('port'));
    });

    Setting Up The Front End

    In this section, we will accomplish the following.

    • Add an entry index.jade and include head and footer layouts.
    • Add gulp tasks to automate the frontend workflow we discussed above.
    • Update .bowerrc file to automatically inject installed packages to our html layout.

    Add index.jade

    Being a Single Page Application (SPA), Bookmark will be served from a single entry file /public/index.html . Let's go ahead and setup the corresponding app/index.jade.

    /app/index.jade

    doctype html
    html(lang="en")
      head
        title bookmark app
        include views/layouts/head
      body(layout="row" ng-app="Bookmark")
        include views/layouts/footer

    /app/views/layouts/head.jade

    meta(charset="utf-8")
    meta(http-equiv="X-UA-Compatible" content="IE=edge")
    meta(name="description" content="")
    meta(name='HandheldFriendly' content='True')
    meta(name='MobileOptimized' content='320')
    meta(name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
    
    base(href="/")
    
    // CDN provided
    link(rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic" type="text/css")
    link(rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" type="text/css")
    
    // bower:css
    // endbower
    
    // inject:css
    // endinject

    /app/views/layouts/footer.jade

    // bower:js
    // endbower
    
    // inject:js
    // endinject

    We will be using wiredep and gulp-inject packages to inject the included files in the right order of dependencies automatically as they are installed as is demonstrated in this short video.

    Adding The Gulp Tasks

    Adding gulp tasks is a very crucial point in our application development process. Without our gulpfile, all the code we write is never compiled into our public directory which serves our application on the user's browsers.

    Go ahead and install the packages below as dev dependencies and update your gulpfile.

    npm install --save-dev gulp gulp-jade gulp-less wiredep gulp-inject gulp-concat gulp-uglify gulp-bower gulp-nodemon gulp-plumber gulp-cssmin

    This maybe overwhelming if you are not familiar with gulp tasks, take some time to read through automating tasks with gulp.

    /gulpfile.js

    var gulp = require('gulp'),
      jade = require('gulp-jade'),
      less = require('gulp-less'),
      wiredep = require('wiredep').stream,
      gulpinject = require('gulp-inject')
      concat = require('gulp-concat'),
      uglify = require('gulp-uglify'),
      bower = require('gulp-bower'),
      nodemon = require('gulp-nodemon'),
      plumber = require('gulp-plumber'),
      cssmin = require('gulp-cssmin'),
      mocha = require('gulp-mocha'),
      istanbul = require('gulp-istanbul'),
      karma = require('karma').Server,
      path = require('path');
    
    //Paths to watch for changes using the watch task.
    var paths = {
      jade: 'app/**/*.jade',
      index: 'public/index.html',
      images: ['app/images/**/*'],
      scripts: {
        js: './public/js/index.js',
        css: './public/css/*.css'
      },
      compileScripts: {
        js: ['app/app.js', 'app/js/**/*.js'],
        css: 'app/styles/*.+(less|css)'
      },
      serverTests: [
        'test/server/**/*.js'
      ],
      serverScripts: [
        'server/controllers/*.js'
      ]
    };
    
    //Compile Jade files to html and save them into the public directory.
    gulp.task('jade:compile', function () {
      gulp.src(paths.jade)
        .pipe(jade({
          pretty: true
        }))
        .pipe(gulp.dest('./public'));
    });
    
    //Concatinate js into index.js, minify and save in public/js.
    gulp.task('js:minify', function () {
      gulp.src(paths.compileScripts.js)
        .pipe(concat('index.js'))
        .pipe(uglify())
        .pipe(gulp.dest('./public/js/'));
    });
    
    //Concatinate custom css into styles.css, minify and save in public/css.
    gulp.task('css:minify', function () {
      gulp.src(paths.compileScripts.css)
        .pipe(less({
          paths: [path.join(__dirname, 'styles')]
        }))
        .pipe(concat('styles.css'))
        .pipe(cssmin())
        .pipe(plumber())
        .pipe(gulp.dest('./public/css'));
    });
    
    //Copy the images folder from app to public recursively
    gulp.task('copy:images', function () {
      gulp.src(paths.images)
        .pipe(gulp.dest('./public/images'));
    });
    
    //Run bower install.
    gulp.task('bower:run', function () {
      bower();
    });
    
    //Inject bower scripts and custom scripts into /public/index.html.
    gulp.task('scripts:inject', ['jade:compile'], function () {
      gulp.src(paths.index)
        .pipe(wiredep())
        .pipe(gulpinject(gulp.src(paths.scripts.js), { relative: true }))
        .pipe(gulpinject(gulp.src(paths.scripts.css), { relative: true }))
        .pipe(gulp.dest('./public/'));
    });
    
    //Rn nodemon.
    gulp.task('nodemon:run', function () {
      nodemon({
        script: 'index.js',
        ext: 'js html',
        ignore: ['public/**', 'app/**', 'node_modules/**']
      });
    });
    
    gulp.task('test:client', function (done) {
      new karma({
        configFile: __dirname + '/karma.conf.js'
      }, done).start();
    });
    
    //Run the server tests and generate coverage reports
    gulp.task('test:server', ['test:server:coverage'], function (done) {
      gulp.src(paths.serverTests)
        .pipe(mocha())
        .pipe(istanbul.writeReports({
          dir: './coverage/server',
          reporters: ['lcov', 'json', 'text', 'text-summary']
        }));
    });
    
    gulp.task('test:server:coverage', function () {
      gulp.src(paths.serverScripts)
        .pipe(istanbul())
        .pipe(istanbul.hookRequire());
    });
    
    //Watch for changes in files.
    gulp.task('watch', function () {
      gulp.watch(paths.jade, ['jade:compile']);
      gulp.watch(paths.compileScripts.js, ['js:minify']);
      gulp.watch(paths.compileScripts.css, ['css:minify']);
      gulp.watch(paths.index, ['scripts:inject']);
      gulp.watch(paths.images, ['copy:images'])
    });
    
    //Default task.
    gulp.task('default', ['bower:run', 'jade:compile', 'js:minify', 'css:minify', 'scripts:inject', 'copy:images']);
    
    //Dev environment task.
    gulp.task('dev', ['nodemon:run', 'bower:run', 'jade:compile', 'js:minify', 'css:minify', 'scripts:inject', 'watch', 'copy:images']);

    Update .bowerrc

    Set the default directory where bower components will be installed and run the inject gulp task after bower components are installed.

    {
      "directory": "public/lib",
      "scripts": {
        "postinstall":"node_modules/.bin/gulp scripts:inject"
      }
    }

    Initializing And Theming Our Bookmark Application

    We will use Angular material's mdThemingProvider and mdIconProvider services to theme our application and set a placeholder svg icon for our authors respectively.

    /app/app.js

    angular.module('Bookmark.controllers', []);
    angular.module('Bookmark.services', []);
    
    //Inject dependencies to the Bookmark module.
    var Bookmark = angular.module('Bookmark', [
      'ngMaterial',
      'ngMdIcons',
      'Bookmark.controllers',
      'Bookmark.services'
    ]);
    
    Bookmark
      .config(['$mdThemingProvider', function ($mdThemingProvider) {
        //Set default theme
        $mdThemingProvider.theme('default')
          .primaryPalette('blue')
          .accentPalette('red');
      }])
      .config(['$mdIconProvider', function ($mdIconProvider) {
        //Placeholder icon for author profile.
        $mdIconProvider
          .icon('author', './images/svg/person.svg');
      }]);

    Main Controller And Toast Service

    The main controller handles all requests in our main page when the application is loaded. Besides fetching all authors from the server, it also triggers our dialogs to add, edit or delete books and authors.

    /app/js/controllers/main.js

    angular.module('Bookmark.controllers')
    .controller('MainCtrl', ['$rootScope', '$scope', '$mdSidenav', '$mdDialog', 'Authors', 'Books', 'Toast',
    function ($rootScope, $scope, $mdSidenav, $mdDialog, Authors, Books, Toast) {
      //Fetch all authors
      Authors.all()
        .then(function (authors) {
          $rootScope.authors = authors;
          $rootScope.selectedAuthor = authors[0];
        });
    
      //set an author as selected
      $scope.selectAuthor = function (author) {
        $rootScope.selectedAuthor = author;
      };
    
      //toggle the visibility of the Sidenav
      $scope.toggleSidenav = function () {
        $mdSidenav('left').toggle();
      };
    
      //Dialog to create new author
      $scope.newAuthorDialog = function (ev) {
        $mdDialog.show({
          templateUrl: 'views/dialogs/new-author.html',
          controller: 'AuthorDialogCtrl',
          parent: angular.element(document.body),
          targetEvent: ev,
          clickOutsideToClose:true,
          fullscreen: true
        });
      };
    
      //Show An author's profile
      $scope.showAuthorProfile = function (ev) {
        $mdDialog.show({
          templateUrl: 'views/dialogs/author-profile.html',
          controller: 'AuthorDialogCtrl',
          parent: angular.element(document.body),
          targetEvent: ev,
          clickOutsideToClose:true,
          fullscreen: true
        });
      };
    
      //Dialog to create new book
      $scope.newBookDialog = function (ev) {
        $mdDialog.show({
          templateUrl: 'views/dialogs/new-book.html',
          controller: 'BookDialogCtrl',
          locals: {
            bookDetails: null
          },
          parent: angular.element(document.body),
          targetEvent: ev,
          clickOutsideToClose:true,
          fullscreen: true
        });
      };
    
      //Delete an existing book
      $scope.deleteBook = function (ev, book) {
        var deleteBook = $mdDialog.confirm()
          .title('Delete ' + book.name + '?')
          .textContent(book.name + ' will be wiped off the face of the earth.')
          .ariaLabel('Delete book')
          .ok('Yes! do it!')
          .cancel('No');
    
        $mdDialog.show(deleteBook)
          .then(function () {
            Books.delete(book.id)
              .then(function () {
                $scope.selectedAuthor.Books = $scope.selectedAuthor.Books.filter(function (i) {
                  return i.id !== book.id;
                });
                Toast.show(book.name + ' has been deleted.', 'top right', 3000);
              })
              .catch(function (error) {
                Toast.show('Error deleting ' + book.name + '. Please try again.', 'top right', 3000);
              });
    
          });
      };
    
      //Show modal to edit an existing book's details
      $scope.editBook = function (ev, book) {
        $mdDialog.show({
          templateUrl: 'views/dialogs/edit-book.html',
          controller: 'BookDialogCtrl',
          locals: {
            bookDetails: book
          },
          parent: angular.element(document.body),
          targetEvent: ev,
          clickOutsideToClose:true,
          fullscreen: true
        });
      };
    }]);

    The toast service simply displays an angular toast notification on actions such as adding, editing or deleting authors and books.

    app/js/services/toast.js

    angular.module('Bookmark.services')
      .factory('Toast', ['$mdToast', function ($mdToast) {
        return {
          show: function (text, position, delay) {
            $mdToast.show(
              $mdToast.simple()
                .textContent(text)
                .position(position)
                .hideDelay(delay)
              );
          }
        };
      }]);

    Author Controller And Service

    The Author dialog controller has one function that makes a request to the new method in the Author service which creates a new author in our database.

    We then use the toast service defined earlier to show a success or error message.

    app/js/controllers/author-dialog.js

    angular.module('Bookmark.controllers')
      .controller('AuthorDialogCtrl', ['$rootScope', '$scope', '$mdDialog', 'Toast', 'Authors',
      function ($rootScope, $scope, $mdDialog, Toast, Authors) {
        //Save a new author
        $scope.saveAuthor = function (author) {
          Authors
            .new(author)
            .then(function (newAuthor) {
              newAuthor.Books = [];
              $rootScope.authors.push(newAuthor);
              Toast.show('Author successfully created', 'top right', 3000);
              $mdDialog.cancel();
            })
            .catch(function () {
              Toast.show('Error creating author', 'top right', 3000);
            });
        };
      }]);

    app/js/services/authors.js

    angular.module('Bookmark.services')
      .factory('Authors', ['$http', '$q', function ($http, $q) {
        return {
          all: function() {
            var deferred = $q.defer();
            $http
              .get('/authors')
              .then(function (response) {
                deferred.resolve(response.data);
              })
              .catch(function (error) {
                deferred.reject(error);
              });
              return deferred.promise;
          },
    
          //add new author
          new: function (author) {
            var deferred = $q.defer();
            $http
              .post('/authors', author)
              .then(function (response) {
                deferred.resolve(response.data);
              })
              .catch(function (error) {
                deferred.reject(error);
              });
            return deferred.promise;
          }
        };
      }]);

    Books Controller And Service

    The books controller has two methods saveBook which creates a new book and updateBook which edits an existing book.

    app/js/controllers/book-dialog.js

    angular.module('Bookmark.controllers')
      .controller('BookDialogCtrl', ['$rootScope', '$scope', '$mdDialog', 'Toast', 'Books', 'bookDetails',
      function ($rootScope, $scope, $mdDialog, Toast, Books, bookDetails) {
        $scope.book = bookDetails;
        if ($scope.book) {
          $scope.book.publication_date = new Date($scope.book.publication_date);
        }
        //create a new book
        $scope.saveBook = function (book) {
          book['author_id'] = $rootScope.selectedAuthor.id;
          Books
            .new(book)
            .then(function (newBook) {
              Toast.show('Book successfully created', 'top right', 3000);
              $rootScope.selectedAuthor.Books.push(newBook);
              $mdDialog.cancel();
            })
            .catch(function () {
              Toast.show('Error creating book', 'top right', 3000);
            });
        };
    
        //Update a book's details
        $scope.updateBook = function (book) {
          Books
            .update(book)
            .then(function () {
              Toast.show('Book successfully updated', 'top right', 3000);
              $mdDialog.cancel();
            })
            .catch(function () {
              Toast.show('Error updating book', 'top right', 300);
            });
        };
      }]);

    The Books service has 3 methods new, delete and update which serves BookDialogCtrl and mainCtrl

    app/js/services/books.js

    angular.module('Bookmark.services')
      .factory('Books', ['$http', '$q', function ($http, $q) {
    
        return {
          new: function (book) {
            var deferred = $q.defer();
            $http
              .post('/books', book)
              .then(function (response) {
                deferred.resolve(response.data);
              })
              .catch(function (error) {
                deferred.reject(error);
              });
            return deferred.promise;
          },
          delete: function(bookId) {
            var deffered = $q.defer();
            $http
              .delete('/books/' + bookId)
              .then(function (response) {
                deffered.resolve(response.data);
              })
              .catch(function (error) {
                deffered.reject(error);
              });
            return deffered.promise;
          },
          update: function (book) {
            var deferred = $q.defer();
            $http
              .put('/books/' + book.id, book)
              .then(function (response) {
                deferred.resolve(response.data);
              })
              .catch(function (error) {
                deferred.reject(error);
              });
            return deferred.promise;
          }
        };
      }]);

    Designing The Application Layout

    Sidenav and navigation layouts

    The sidenav contains a clickable list of all the authors in the Bookmark application. /app/index.jade

    doctype html
    html(lang="en")
      head
        title bookmark App
        include views/layouts/head
      body(layout="row" ng-app="Bookmark" ng-controller="MainCtrl")
    
        //Sidenav
        md-sidenav.md-whiteframe-z2(md-component-id="left" md-is-locked-open="$mdMedia('gt-sm')")
          md-toolbar
            h1 Authors
          md-content
            md-list
               //Loop through the authors list
              md-list-item(ng-repeat="author in authors")
                md-button(ng-click="selectAuthor(author)")
                  md-icon.author_avatar(md-svg-icon="author")
                  span(ng-class="{selected: author === selectedAuthor}") {{author.name}}
          div.new_author
            md-button(aria-label="new_author" class="md-fab md-raised md-primary md-mini")
              ng-md-icon(icon="add")
                md-tooltip(direction="top") Add new author
    
        //Main toolbar
        div(layout="column" flex)
          md-toolbar(layout="row")
            div.md-toolbar-tools
              md-button.md-button-icon(hide-gt-sm aria-label="Menu" ng-click="toggleSidenav()")
                ng-md-icon.authors(icon="view_module")
              h1 Bookmark
    
          //Main content
        include views/layouts/footer

    I will be using Angular material icons so make sure you go ahead and bower install the icons package bower install --save angular-material-icons.

    If you run gulp dev on your terminal now, we should now have this simple layout.

    Bookmark navigation and toolbar

    Main content layout

    The main content section holds the bullk of our application.

    /app/index.jade

          //Main content
          md-content.md-default-theme
            div.md-padding
              h2(ng-show="selectedAuthor.Books.length") Books by {{selectedAuthor.name}}
              div.author_actions
                md-button.author_profile(aria-label="new_book" class="md-fab md-raised md-primary md-mini")
                  ng-md-icon(icon="person")
                    md-tooltip(direction="bottom") View {{selectedAuthor.name}}'s profile
                md-button.new_book(aria-label="new_book" class="md-fab md-raised md-primary md-mini")
                  ng-md-icon(icon="my_library_add")
                    md-tooltip(direction="bottom") Add new book by {{selectedAuthor.name}}
            md-content.md-padding(layout="column")
              md-card(ng-repeat="book in selectedAuthor.Books")
                div(layout="row" layout-align="end")
                  div.books_menu
                      md-button(aria-label="edit" class="md-fab md-raised md-mini")
                        ng-md-icon(icon="edit")
                        md-tooltip(direction="bottom") edit book details
                      md-button(aria-label="delete" class="md-fab md-raised md-mini")
                        ng-md-icon(icon="delete")
                        md-tooltip(direction="bottom") delete book details
                md-card-title
                  md-card-title-text
                    span.md-headline {{book.name}}
                    span.md-subhead Publication date: {{ book.publication_date | date: mediumDate}}
                md-card-content
                  div.md-subhead {{book.description}}
                  md-chips
                    md-chip ISBN Number: {{ book.isbn}}

    While we are yet to wire up the functions of most of the buttons, run gulp dev to view the updated layout.

    Bookmark app main content layout

    Creating An Author and Adding New Books

    With the layout looking arguably swanky, let's dive into the meat of our application. In this section, we will manage to create a new author from a form and by extension, adding a new book to an author's profile.

    To get started, add a click event to the new author button.

    /app/index.jade

    div.new_author
            //Show dialog to create new author on click.
            md-button(aria-label="new_author" class="md-fab md-raised md-primary md-mini" ng-click="newAuthorDialog($event)")
              ng-md-icon(icon="add")
                md-tooltip(direction="top") Add new author

    The newAuthorDialog method in our mainCtrl pops up a dialog box that will contain our new author form. Read more about all the possible $mdDialog options in the official angular material documentation.

    To display the dialog that creates a new author, add the new-author template.

    /app/views/dialogs/new-author.jade

    md-dialog(aria-label="New Author" flex="50")
      md-toolbar
        .md-toolbar-tools
          h2 Create New Author
      form(name="authorForm" ng-submit="saveAuthor(author)" novalidate)
        md-dialog-content.md-padding
          md-input-container.md-block
            label Name
            input(ng-model="author.name" type="text" name="author.name" required)
          md-input-container.md-block
            label Biography
            textarea(ng-model="author.bio" rows="5" name="author.bio" required)
        md-dialog-actions
          md-button(aria-label="Save Author" type="submit" ng-disabled="authorForm.$invalid") Save

    To display the dialog to create a new book, add the new-book template.

    app/views/dialogs/new-book.jade

     md-dialog(aria-label="New Book" flex="50")
      md-toolbar
        .md-toolbar-tools
          h2 Add New Book
      form(name="bookForm" ng-submit="saveBook(book)" novalidate)
        md-dialog-content.md-padding
          div(layout="row")
            md-input-container(flex)
              label Name
              input(ng-model="book.name" type="text" name="book.name" required)
            md-input-container(flex)
              label Publication Date
              md-datepicker(ng-model="book.publication_date" type="text" name="book.publication_date" required)
          md-input-container.md-block
            label ISBN number
            input(ng-model="book.isbn" type="text" name="book.isbn" required)
          md-input-container.md-block
            label Description
            textarea(ng-model="book.description" rows="5" name="book.description" required)
        md-dialog-actions
          md-button(aria-label="Save Book" type="submit" ng-disabled="bookForm.$invalid") Add Book

    On clicking the submit, the saveAuthor method in AuthorDialogCtrl is called and the newly added details are passed to it which in turns calls the new method in our Authors service.

    Thanks to Angular's two way data binding , the author is automatically visible to our sidenav when we push the author details to the authors variable in rootScope.

    Viewing An Author's Profile

    Just like we did while creating a new author, add a new event handler to pop the dialog containing the author's profile.

    /app/index.jade

    div.author_actions
                md-button.author_profile(aria-label="view_profile" class="md-fab md-raised md-primary md-mini" ng-click="showAuthorProfile($event)")
                  ng-md-icon(icon="person")
                    md-tooltip(direction="bottom") View {{selectedAuthor.name}}'s profile

    To display the dialog that shows an author's profile, add the author-profile template.

    app/views/dialogs/author-profile.jade

    md-dialog(aria-label="Author Profile" flex="50")
      md-toolbar
        .md-toolbar-tools
          h2 {{$root.selectedAuthor.name}}'s profile
      md-dialog-content.md-padding
        md-list
          md-list-item
            div.md-list-item-text
              h3 Biography
              p {{$root.selectedAuthor.bio}}
          md-divider
          md-list-item
            div.md-list-item-text
              h3 Books on Bookmark
              p {{$root.selectedAuthor.Books.length}}

    On clicking the view profile button, the User's profile will be displayed as shown below.

    Run gulp dev on your terminal to try this out.

    View Author Profile

    Updating And Deleting An Existing Book

    To edit and delete an book, add click events to the edit and delete button for each book.

    /app/index.jade

    //Edit and Delete Books
    div.books_menu
      md-button(aria-label="edit" class="md-fab md-raised md-mini" ng-click="editBook($event, book)")
        ng-md-icon(icon="edit")
        md-tooltip(direction="bottom") edit book details
      md-button(aria-label="delete" class="md-fab md-raised md-mini" ng-click="deleteBook($event, book)")
         ng-md-icon(icon="delete")
         md-tooltip(direction="bottom") delete book

    We already have the deleteBook method defined in MainCtrl which presents the user with a confirmation dialog to either accept or cancel the delete request.

    To show the dialog with the edit book form, let's add the edit-book dialog template.

    /app/views/dialogs/edit-book.jade

    md-dialog(aria-label="Edit Book" flex="50")
      md-toolbar
        .md-toolbar-tools
          h2 Edit {{book.name}} {{book.publication_date | date: 'm/d/yyyy'}}
      form(name="bookForm" ng-submit="updateBook(book)" novalidate)
        md-dialog-content.md-padding
          div(layout="row")
            md-input-container(flex)
              label Name
              input(ng-model="book.name" type="text" name="book.name" required)
            md-input-container(flex)
              label Publication Date
              md-datepicker(ng-model="book.publication_date" type="text" name="book.publication_date" required)
          md-input-container.md-block
            label ISBN number
            input(ng-model="book.isbn" type="text" name="book.isbn" required)
          md-input-container.md-block
            label Description
            textarea(ng-model="book.description" rows="5" name="book.description" required)
        md-dialog-actions
          md-button(aria-label="Save Book" type="submit" ng-disabled="bookForm.$invalid") Update Book

    At this point, our Bookmark application performs the basic CRUD operations for the authors and the books.

    Conclusion

    In this section, we managed to:

    • Setup the frontend for our Bookmark application.
    • Theme and define our app layout
    • Create, view, update and delete book details.

    Just like we did in the first part of this series, you may also challenge yourself to edit and delete an author's details.

    In the last part of the series, we will right tests for our app and deploy it to Heroku.

    John Kariuki

    22 posts

    Software developer at Andela.

    PHP, JavaScript developer (AngularJS, React, Node.JS)

    Avid blog reader and fascinated by drones.

    I play basketball, swim and jog in my free time.