Creating An AngularJS Application With Sequelize – Part 2

The second part of a three-part series in building an Angular JS application with Sequelize.

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

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.

├── 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

Software developer at Andela. Proficient in PHP with Laravel and Codeigniter.

Conversant with MEAN(MongoDB, Express.js, AngularJS, Node.js) and currently learning Python and Go.

Avid blog reader and fascinated by drones.

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