├── .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 |
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 |
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 | Book Name |
7 | Author |
8 |
9 |
10 | {{book.title}} |
11 | {{book.author}} |
12 |
13 |
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 | Book Name |
16 | Author |
17 | Mark Read |
18 | Add to Archive |
19 |
20 |
21 | {{book.title}} |
22 | {{book.author}} |
23 |
24 |
25 | |
26 |
27 |
28 | |
29 |
30 |
31 |
--------------------------------------------------------------------------------