├── .gitignore ├── Index.html ├── app.js ├── app ├── Controllers │ ├── AddBookController.js │ ├── ArchiveController.js │ ├── HomeController.js │ └── bookShelf.controllers.js ├── bookShelf.directives.js ├── bookShelf.main.js ├── bookShelf.services.js └── bootstrap.js ├── bower.json ├── gulpfile.js ├── package.json ├── readme.md ├── server ├── bookShelfApi.js └── bookShelfData.js └── templates ├── addBook.html ├── archive.html └── home.html /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ -------------------------------------------------------------------------------- /Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home 6 | 7 | 30 | 31 | 32 |
33 | 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | 4 | var bookShelfApi = require('./server/bookShelfApi'); 5 | 6 | var app = express(); 7 | app.use(bodyParser()); 8 | app.use(express.static('.')); 9 | 10 | app.get('/api/activeBooks', function (request, response) { 11 | response.send(bookShelfApi.getBooks(false)); 12 | }); 13 | 14 | app.get('/api/archivedBooks', function (request, response) { 15 | response.send(bookShelfApi.getBooks(true)); 16 | }); 17 | 18 | app.post('/api/books', function (request, response) { 19 | var newBook=request.body; 20 | if(!newBook.title){ 21 | response.send(500,{errorText:'No data found to add'}); 22 | } 23 | else { 24 | bookShelfApi.addABook(newBook); 25 | response.send(200, {message: 'New book added to the list'}); 26 | } 27 | }); 28 | 29 | app.put('/api/markRead/:id', function (request, response) { 30 | if(!request.params.id){ 31 | response.send(500,{errorText:'BookId not sent'}); 32 | } 33 | else if(request.body.read === undefined || request.body.read === null){ 34 | response.send(500,{errorText:'No data found to edit'}); 35 | } 36 | else{ 37 | var modifiedBook = bookShelfApi.modifyReadStatus(parseInt(request.params.id), request.body.read); 38 | if(modifiedBook === null){ 39 | response.send(500,{errorText:'Book not found in the list'}); 40 | } 41 | else{ 42 | response.send(modifiedBook); 43 | } 44 | } 45 | }); 46 | 47 | app.put('/api/addToArchive/:id', function (request, response) { 48 | if(!request.params.id){ 49 | response.send(500,{errorText:'Can\'t update book if id is not sent'}); 50 | } 51 | else{ 52 | var modifiedBook = bookShelfApi.addToArchive(parseInt(request.params.id)); 53 | if(modifiedBook === null){ 54 | response.send(500,{errorText:'Book not found in the list'}); 55 | } 56 | else{ 57 | response.send(modifiedBook); 58 | } 59 | } 60 | }); 61 | 62 | app.get('/api/bookExists/:title', function (request, response) { 63 | if(!request.params.title){ 64 | response.send(500, {errorText:'Can\'t check existence of book if title is not sent'}); 65 | } 66 | else{ 67 | response.send(bookShelfApi.bookExists(request.params.title)); 68 | } 69 | }); 70 | 71 | app.get('/', function (request, response) { 72 | response.sendfile('Index.html'); 73 | }); 74 | 75 | app.listen(8000, function () { 76 | console.log('Express server started!!!'); 77 | }); 78 | -------------------------------------------------------------------------------- /app/Controllers/AddBookController.js: -------------------------------------------------------------------------------- 1 | const SERVICE = new WeakMap(); 2 | const TIMEOUT = new WeakMap(); 3 | 4 | class AddBookController{ 5 | constructor($timeout, bookShelfSvc){ 6 | TIMEOUT.set(this, $timeout); 7 | SERVICE.set(this, bookShelfSvc); 8 | } 9 | 10 | addBook(){ 11 | if(this.addBookForm.$valid && this.book !== {}){ 12 | SERVICE.get(this).addBook(this.book).then(message => { 13 | this.addSuccess = true; 14 | TIMEOUT.get(this)(() => { 15 | this.addSuccess = false; 16 | }, 2500); 17 | this.resetBook(); 18 | }, error => { 19 | this.addFailed = true; 20 | TIMEOUT.get(this)(() => { 21 | this.addFailed = false; 22 | }, 2500); 23 | }); 24 | } 25 | } 26 | 27 | resetBook(){ 28 | this.addBookForm.$setPristine(); 29 | this.addBookForm.$setUntouched(); 30 | this.book = {}; 31 | } 32 | } 33 | 34 | AddBookController.$inject = ['$timeout', 'bookShelfSvc']; 35 | 36 | export default AddBookController; -------------------------------------------------------------------------------- /app/Controllers/ArchiveController.js: -------------------------------------------------------------------------------- 1 | const INIT = new WeakMap(); 2 | const SERVICE = new WeakMap(); 3 | 4 | class ArchiveController{ 5 | constructor(bookShelfSvc){ 6 | SERVICE.set(this, bookShelfSvc); 7 | 8 | INIT.set(this, () =>{ 9 | SERVICE.get(this).getArchivedBooks().then(books => { 10 | this.books = books; 11 | }); 12 | }); 13 | 14 | INIT.get(this)(); 15 | } 16 | } 17 | 18 | ArchiveController.$inject = ['bookShelfSvc']; 19 | 20 | export default ArchiveController; -------------------------------------------------------------------------------- /app/Controllers/HomeController.js: -------------------------------------------------------------------------------- 1 | const INIT = new WeakMap(); 2 | const SERVICE = new WeakMap(); 3 | const TIMEOUT = new WeakMap(); 4 | 5 | class HomeController{ 6 | constructor($timeout, bookShelfSvc){ 7 | SERVICE.set(this, bookShelfSvc); 8 | TIMEOUT.set(this, $timeout); 9 | INIT.set(this, () => { 10 | SERVICE.get(this).getActiveBooks().then(books => { 11 | this.books = books; 12 | }); 13 | }); 14 | 15 | INIT.get(this)(); 16 | } 17 | 18 | markBookAsRead(bookId, isBookRead){ 19 | return SERVICE.get(this).markBookRead(bookId, isBookRead) 20 | .then(() => { 21 | INIT.get(this)(); 22 | this.readSuccess = true; 23 | this.readSuccessMessage = isBookRead ? "Book marked as read." : "Book marked as unread."; 24 | TIMEOUT.get(this)(() => { 25 | this.readSuccess = false; 26 | }, 2500); 27 | }); 28 | } 29 | 30 | addToArchive(bookId){ 31 | return SERVICE.get(this).addToArchive(bookId) 32 | .then(() => { 33 | INIT.get(this)(); 34 | this.archiveSuccess = true; 35 | TIMEOUT.get(this)(() => { 36 | this.archiveSuccess = false; 37 | }, 2500); 38 | }); 39 | } 40 | } 41 | 42 | HomeController.$inject = ['$timeout', 'bookShelfSvc']; 43 | 44 | export default HomeController; -------------------------------------------------------------------------------- /app/Controllers/bookShelf.controllers.js: -------------------------------------------------------------------------------- 1 | import HomeController from './HomeController'; 2 | import AddBookController from './AddBookController'; 3 | import ArchiveController from './ArchiveController'; 4 | 5 | var moduleName='bookShelf.controllers'; 6 | 7 | angular.module(moduleName, []) 8 | .controller('bookShelf.homeController', HomeController) 9 | .controller('bookShelf.addBookController', AddBookController) 10 | .controller('bookShelf.archiveController', ArchiveController); 11 | 12 | export default moduleName; -------------------------------------------------------------------------------- /app/bookShelf.directives.js: -------------------------------------------------------------------------------- 1 | var moduleName='bookShelf.directives'; 2 | 3 | const Q = new WeakMap(); 4 | const SERVICE = new WeakMap(); 5 | 6 | class UniqueBookTitle 7 | { 8 | constructor($q, bookShelfSvc){ 9 | this.require='ngModel'; 10 | this.restrict='A'; 11 | 12 | Q.set(this, $q); 13 | SERVICE.set(this, bookShelfSvc); 14 | } 15 | 16 | link(scope, elem, attrs, ngModelController){ 17 | ngModelController.$asyncValidators.uniqueBookTitle = function(value){ 18 | 19 | return Q.get(UniqueBookTitle.instance)((resolve, reject) => { 20 | SERVICE.get(UniqueBookTitle.instance).checkIfBookExists(value).then( result => { 21 | if(result){ 22 | reject(); 23 | } 24 | else{ 25 | resolve(); 26 | } 27 | }); 28 | }); 29 | }; 30 | } 31 | 32 | static directiveFactory($q, bookShelfSvc){ 33 | UniqueBookTitle.instance =new UniqueBookTitle($q, bookShelfSvc); 34 | return UniqueBookTitle.instance; 35 | } 36 | } 37 | 38 | UniqueBookTitle.directiveFactory.$inject = ['$q', 'bookShelfSvc']; 39 | 40 | angular.module(moduleName, []) 41 | .directive('uniqueBookTitle', UniqueBookTitle.directiveFactory); 42 | 43 | export default moduleName; 44 | -------------------------------------------------------------------------------- /app/bookShelf.main.js: -------------------------------------------------------------------------------- 1 | import { default as controllersModuleName } from './Controllers/bookShelf.controllers'; 2 | import { default as servicesModuleName } from './bookShelf.services'; 3 | import { default as directivesModuleName } from './bookShelf.directives'; 4 | 5 | var moduleName = 'bookShelf'; 6 | 7 | function config($routeProvider){ 8 | $routeProvider 9 | .when('/',{ 10 | templateUrl:'templates/home.html', 11 | controller:'bookShelf.homeController', 12 | controllerAs:'vm' 13 | }) 14 | .when('/addBook',{ 15 | templateUrl:'templates/addBook.html', 16 | controller:'bookShelf.addBookController', 17 | controllerAs:'vm' 18 | }) 19 | .when('/archive', { 20 | templateUrl:'templates/archive.html', 21 | controller:'bookShelf.archiveController', 22 | controllerAs:'vm' 23 | }) 24 | .otherwise({redirectTo:'/'}); 25 | } 26 | 27 | config.$inject = ['$routeProvider']; 28 | 29 | var app = angular.module(moduleName, ['ngRoute','ngMessages', servicesModuleName, controllersModuleName, directivesModuleName]) 30 | .config(config); 31 | 32 | export default moduleName; -------------------------------------------------------------------------------- /app/bookShelf.services.js: -------------------------------------------------------------------------------- 1 | var moduleName='bookShelf.services'; 2 | 3 | const HTTP = new WeakMap(); 4 | 5 | class BookShelfService 6 | { 7 | constructor($http) 8 | { 9 | HTTP.set(this, $http); 10 | } 11 | 12 | getActiveBooks(){ 13 | return HTTP.get(this).get('/api/activeBooks').then(result => result.data ); 14 | } 15 | 16 | getArchivedBooks(){ 17 | return HTTP.get(this).get('/api/archivedBooks').then(result => result.data ); 18 | } 19 | 20 | markBookRead(bookId, isBookRead){ 21 | return HTTP.get(this).put(`/api/markRead/${bookId}`, {bookId: bookId, read: isBookRead}); 22 | } 23 | 24 | addToArchive(bookId){ 25 | return HTTP.get(this).put(`/api/addToArchive/${bookId}`,{}); 26 | } 27 | 28 | checkIfBookExists(title){ 29 | return HTTP.get(this).get(`/api/bookExists/${title}`).then(result => result.data ); 30 | } 31 | 32 | addBook(book){ 33 | return HTTP.get(this).post('/api/books', book); 34 | } 35 | 36 | static bookShelfFactory($http){ 37 | return new BookShelfService($http); 38 | } 39 | } 40 | 41 | BookShelfService.bookShelfFactory.$inject = ['$http']; 42 | 43 | angular.module(moduleName, []) 44 | .factory('bookShelfSvc', BookShelfService.bookShelfFactory); 45 | 46 | export default moduleName; 47 | -------------------------------------------------------------------------------- /app/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { default as bookShelfModule} from './app/bookShelf.main'; 2 | angular.bootstrap(document, [bookShelfModule]); 3 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BookShelf", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "jquery": "~2.1.0", 6 | "angular": "~1.3.0", 7 | "angular-animate": "~1.3.0", 8 | "angular-route": "~1.3.0", 9 | "angular-sanitize": "~1.3.0", 10 | "bootstrap": "~3.2.0", 11 | "traceur": "0.0.79", 12 | "angular-messages": "~1.3.8" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | server = require('gulp-express'); 3 | 4 | gulp.task('server', function () { 5 | return server.run({ 6 | file: 'app.js' 7 | }); 8 | }); 9 | 10 | gulp.task('default', ['server']); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BookShelfApp", 3 | "version": "0.0.1", 4 | "configs": { 5 | "gulp": "./node_modules/.bin/gulp" 6 | }, 7 | "scripts": { 8 | "gulp": "gulp" 9 | }, 10 | "devDependencies": { 11 | "body-parser": "^1.10.1", 12 | "express": "^4.10.6", 13 | "gulp": "^3.8.10", 14 | "gulp-express": "^0.1.4", 15 | "underscore": "^1.7.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ##Running the code 2 | To run the sample, open a command prompt and execute the following commands: 3 | 4 | - npm install (to install node.js dependencies) 5 | - bower install (to install front-end packages) 6 | - npm run gulp (to start the server) 7 | - Open a browser and change the URL to http://localhost:8000 8 | 9 | To understand internals of this code sample, read my Sitepoint article on Writing Angular Apps using ES6. 10 | -------------------------------------------------------------------------------- /server/bookShelfApi.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var books = require('./bookShelfData').books; 3 | 4 | function addABook(newBook) { 5 | var lastBook = _.max(books, function (book) { 6 | return book.bookId; 7 | }); 8 | 9 | newBook.bookId = lastBook.bookId + 1; 10 | newBook.read = false; 11 | newBook.archived = false; 12 | 13 | books.push(newBook); 14 | return newBook; 15 | } 16 | 17 | function getBooks(isArchived) { 18 | return _.filter(books, function (book) { 19 | return book.archived === isArchived; 20 | }); 21 | } 22 | 23 | function modifyBook(modifiedBook) { 24 | var bookIndex = -1; 25 | var bookInCollection = _.find(books, function (book) { 26 | bookIndex++; 27 | return book.bookId === modifiedBook.bookId; 28 | }); 29 | 30 | if(!bookInCollection){ 31 | return null; 32 | } 33 | else{ 34 | books[bookIndex] = modifiedBook; 35 | return modifiedBook; 36 | } 37 | } 38 | 39 | function modifyReadStatus(bookId, isRead){ 40 | var bookIndex = -1; 41 | var bookInCollection = _.find(books, function (book) { 42 | bookIndex++; 43 | return book.bookId === bookId; 44 | }); 45 | 46 | if(!bookInCollection){ 47 | return null; 48 | } 49 | else{ 50 | books[bookIndex].read = isRead; 51 | return books[bookIndex]; 52 | } 53 | } 54 | 55 | function addToArchive(bookId){ 56 | var bookIndex = -1; 57 | var bookInCollection = _.find(books, function (book) { 58 | bookIndex++; 59 | return book.bookId === bookId; 60 | }); 61 | 62 | if(!bookInCollection){ 63 | return null; 64 | } 65 | else{ 66 | books[bookIndex].archived = true; 67 | return books[bookIndex]; 68 | } 69 | } 70 | 71 | function bookExists(title){ 72 | var bookInCollection = _.find(books, function (book) { 73 | return book.title === title; 74 | }); 75 | 76 | return !!bookInCollection; 77 | } 78 | 79 | module.exports = { 80 | getBooks: getBooks, 81 | addABook: addABook, 82 | modifyBook: modifyBook, 83 | modifyReadStatus: modifyReadStatus, 84 | addToArchive: addToArchive, 85 | bookExists: bookExists 86 | }; 87 | -------------------------------------------------------------------------------- /server/bookShelfData.js: -------------------------------------------------------------------------------- 1 | var books = [{ 2 | bookId:1, 3 | title:"Harry Porter and Philosopher's Stone", 4 | author:"JK Rowling", 5 | read:false, 6 | archived: false 7 | }, 8 | { 9 | bookId:2, 10 | title:"The Alchemist", 11 | author:"Paulo Coelho", 12 | read:false, 13 | archived: false 14 | }, 15 | { 16 | bookId:3, 17 | title:"The Passionate Programmer", 18 | author:"Chad Fowler", 19 | read:false, 20 | archived: false 21 | }, 22 | { 23 | bookId:4, 24 | title:"The Shoemaker's Wife", 25 | author:"Adriana Trigiani", 26 | read:false, 27 | archived: false 28 | }, 29 | { 30 | bookId:5, 31 | title:"All the Light We Cannot See", 32 | author:"Anthony Doerr", 33 | read:false, 34 | archived: false 35 | }, 36 | { 37 | bookId:6, 38 | title:"The Winter Sea", 39 | author:"Susanna Kearsley", 40 | read:false, 41 | archived: false 42 | }, 43 | { 44 | bookId:7, 45 | title:"The God Father", 46 | author:"Mario Puzo", 47 | read:false, 48 | archived: false 49 | }, 50 | { 51 | bookId:8, 52 | title:"The Immortals of Meluha", 53 | author:"Amish Tripathi", 54 | read:false, 55 | archived: false 56 | }, 57 | { 58 | bookId:9, 59 | title:"The Winter Sea", 60 | author:"Susanna Kearsley", 61 | read:false, 62 | archived: false 63 | }, 64 | { 65 | bookId: 10, 66 | title: "Code Complete: A Practical Handbook of Software Construction", 67 | author: "Steve McConnell", 68 | read: false, 69 | archived: false 70 | }]; 71 | 72 | module.exports = { 73 | books: books 74 | }; 75 | -------------------------------------------------------------------------------- /templates/addBook.html: -------------------------------------------------------------------------------- 1 |
2 |

Add a new Book

3 |
4 |
5 | Book successfully added. You can find it in the list of active books! 6 |
7 |
8 | Sorry, something went wrong. Adding book was not successful. 9 |
10 |
11 |
12 | Title of Book 13 | 14 |
15 | 16 |
17 | Author 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 |
Title is missing value
29 |
Title already exists
30 |
31 |
32 |
Author is missing value
33 |
34 |
-------------------------------------------------------------------------------- /templates/archive.html: -------------------------------------------------------------------------------- 1 |
2 |

Archived Books

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
Book NameAuthor
{{book.title}}{{book.author}}
14 |
-------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |

List of Books

3 |
4 |
5 |
6 | {{vm.readSuccessMessage}} 7 |
8 |
9 | Book Archived! Now, you can find it in archived list. 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 29 | 30 |
Book NameAuthorMark ReadAdd to Archive
{{book.title}}{{book.author}} 24 | 25 | 27 | 28 |
31 |
--------------------------------------------------------------------------------