MEAN Stack Exercises
Jump to navigation
Jump to search
Angular MEAN stack CRUD Example
- Remember: after each big point below, test your app and check if it still works
- of course if testing such part will be possible
1. Command line initial setup ---------- terminal code starts ----------- sudo npm install -g @angular/cli ng new mean-angular5 cd ./mean-angular5 ng serve CTRL+C npm install --save express body-parser morgan serve-favicon ---------- terminal code ends ----------- 2. Preparing RESTful API with the compiled Angular 5 front end 2.1 Add bin folder and www file inside bin folder. mkdir bin touch bin/www ---------- www file content starts ----------- #!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('mean-app:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); } ---------- www file content ends ----------- 2.2. To make the server run from bin/www, open and edit "package.json" then replace "start" value. ---------- change package.json file content start ----------- ... "scripts": { "ng": "ng", "start": "ng build && node ./bin/www", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, ... ---------- change package.json file content end ----------- 2.3. Create app.js in the root of project folder. touch app.js ---------- app.js file content starts ----------- var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var bodyParser = require('body-parser'); var book = require('./routes/book'); var app = express(); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({'extended':'false'})); app.use(express.static(path.join(__dirname, 'dist'))); app.use('/books', express.static(path.join(__dirname, 'dist'))); app.use('/book', book); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handler app.use( function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); } ); module.exports = app; ---------- app.js file content ends ----------- 2.4. Create routes folder then create routes file for the book mkdir routes touch routes/book.js ---------- book.js file content starts ----------- var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.send('Express RESTful API'); }); module.exports = router; ---------- book.js file content ends ----------- Run the server using this command. npm start In te browser `http://localhost:3000`. and then change the address to `http://localhost:3000/book` Stop the server CTRL+C 3. Install and Configure Mongoose.js npm install --save mongoose bluebird Edit `app.js` then add this lines after another variable line ---------- change app.js file content end ----------- var mongoose = require('mongoose'); mongoose.Promise = require('bluebird'); mongoose.connect( 'mongodb://localhost/mean-angular5', { useMongoClient: true, promiseLibrary: require('bluebird') } ).then(() => console.log('connection succesful')) .catch((err) => console.error(err)); ---------- change app.js file content end ----------- Run MongoDB server on different terminal tab or command line or run from the service sudo service mongod start # on windows: mongod npm start # connection succesful Built-in Mongoose Promise library is depricated, so we add `bluebird` modules and register it as Mongoose Promise library (node:42758) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html 4. Create Mongoose.js Model mkdir models touch models/Book.js ---------- Book.js file content starts ----------- var mongoose = require('mongoose'); var BookSchema = new mongoose.Schema({ isbn: String, title: String, author: String, description: String, published_year: String, publisher: String, updated_date: { type: Date, default: Date.now }, }); module.exports = mongoose.model('Book', BookSchema); ---------- Book.js file content ends ----------- 5. Create Routes for Accessing Book Data via Restful API ---------- Replace routes/book.js file content start ----------- var express = require('express'); var router = express.Router(); var mongoose = require('mongoose'); var Book = require('../models/Book.js'); /* GET ALL BOOKS */ router.get('/', function(req, res, next) { Book.find(function (err, products) { if (err) return next(err); res.json(products); }); }); /* GET SINGLE BOOK BY ID */ router.get('/:id', function(req, res, next) { Book.findById(req.params.id, function (err, post) { if (err) return next(err); res.json(post); }); }); /* SAVE BOOK */ router.post('/', function(req, res, next) { Book.create(req.body, function (err, post) { if (err) return next(err); res.json(post); }); }); /* UPDATE BOOK */ router.put('/:id', function(req, res, next) { Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) { if (err) return next(err); res.json(post); }); }); /* DELETE BOOK */ router.delete('/:id', function(req, res, next) { Book.findByIdAndRemove(req.params.id, req.body, function (err, post) { if (err) return next(err); res.json(post); }); }); module.exports = router; ---------- Replace routes/book.js file content end ----------- 5.1. Populating db npm start # Open the other terminal or command line to test the Restful API by type this command: curl -i -H "Accept: application/json" localhost:3000/book # We expect something similar to this: HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 2 ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w" Date: Fri, 10 Nov 2017 23:53:52 GMT Connection: keep-alive # Populate Book collection with initial data that sent from RESTful API curl -i -X POST -H "Content-Type: application/json" -d '{ "isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author": "Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com" }' localhost:3000/book # Response should look like this HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 415 ETag: W/"19f-SB/dEQyffaTjobOBJbvmwCn7WJA" Date: Fri, 10 Nov 2017 23:58:11 GMT Connection: keep-alive {"__v":0,"isbn":"123442123, 97885654453443","title":"Learn how to build modern web application with MEAN stack","author":"Didin J.","description":"The comprehensive step by step tutorial on how to build MEAN (MongoDB, Express.js, Angular 5 and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com","_id":"5a063d123cf0792af12ce45d","updated_date":"2017-11-10T23:58:10.971Z"}MacBook-Pro:mean-angular5 6. Create Angular 5 Component for Displaying Book List ng g component book 6.1. // ------- Edit `src/app/app.module.ts` -------- import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; // Add it to `@NgModule` imports after `BrowserModule`. imports: [ BrowserModule, FormsModule, HttpClientModule ], // --------------------------------------------- 6.2. // ------- Edit `src/app/book/book.component.ts` -------- import { HttpClient } from '@angular/common/http'; // Inject `HttpClient` to the constructor. constructor(private http: HttpClient) { } // Add array variable for holding books data before the constructor. books: any; // Add a few lines of codes for getting a list of book data from RESTful API inside `ngOnInit` function. ngOnInit() { this.http.get('/book').subscribe(data => { this.books = data; }); } // --------------------------------------------- 6.3. ------- Edit and replace `src/app/book/book.component.html` -------- <div class="container"> <h1>Book List</h1> <table class="table"> <thead> <tr> <th>Title</th> <th>Author</th> <th>Action</th> </tr> </thead> <tbody> <tr *ngFor="let book of books"> <td>{{ book.title }}</td> <td>{{ book.author }}</td> <td>Show Detail</td> </tr> </tbody> </table> </div> --------------------------------------------- 6.3.1. ------- Edit `src/index.html` -------- <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>MeanAngular5</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> </head> <body> <app-root></app-root> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </body> </html> --------------------------------------------- 7. Create Angular 5 Routes to Book Component ------- Edit `src/app/app.module.ts` -------- import { RouterModule, Routes } from '@angular/router'; // Create constant router for routing to book component before `@NgModule`. const appRoutes: Routes = [ { path: 'books', component: BookComponent, data: { title: 'Book List' } }, { path: '', redirectTo: '/books', pathMatch: 'full' } ]; // In @NgModule imports, section adds ROUTES constant, so imports section will be like this. imports: [ BrowserModule, HttpClientModule, RouterModule.forRoot(appRoutes, { enableTracing: true })// <-- debugging purposes only ], --------------------------------------------- 7.1. Activate routes ------- Edit and replace `src/app/app.component.html` -------- <router-outlet></router-outlet> --------------------------------------------- npm start In the browser `http://localhost:3000` `http://localhost:3000/books` 8. Create Angular 5 Component for Displaying Book Detail ng g component book-detail ------- Add route to `src/app/app.module.ts` ------- const appRoutes: Routes = [ { path: 'books', component: BookComponent, data: { title: 'Book List' } }, { path: 'book-details/:id', component: BookDetailComponent, data: { title: 'Book Details' } }, { path: '', redirectTo: '/books', pathMatch: 'full' } ]; --------------------------------------------- 8.1. Update Detail component ts ------- Edit and replace `src/app/book-detail/book-detail.component.ts` -------- import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-book-detail', templateUrl: './book-detail.component.html', styleUrls: ['./book-detail.component.css'], encapsulation: ViewEncapsulation.None }) export class BookDetailComponent implements OnInit { book = {}; constructor(private route: ActivatedRoute, private http: HttpClient) { } ngOnInit() { this.getBookDetail(this.route.snapshot.params['id']); } getBookDetail(id) { this.http.get('/book/'+id).subscribe( data => { this.book = data; }); } } --------------------------------------------- 8.2. Update Detail component html ------- Edit and replace `src/app/book-detail/book-detail.component.html` -------- <div class="container"> <h1>{{ book.title }}</h1> <dl class="list"> <dt>ISBN</dt> <dd>{{ book.isbn }}</dd> <dt>Author</dt> <dd>{{ book.author }}</dd> <dt>Publisher</dt> <dd>{{ book.publisher }}</dd> <dt>Price</dt> <dd>{{ book.price }}</dd> <dt>Update Date</dt> <dd>{{ book.updated_at }}</dd> </dl> </div> --------------------------------------------- 9. Create Angular 5 Component for Add New Book ng g component book-create 9.1. Update router ------- Add route to `src/app/app.module.ts` ------- const appRoutes: Routes = [ { path: 'books', component: BookComponent, data: { title: 'Book List' } }, { path: 'book-details/:id', component: BookDetailComponent, data: { title: 'Book Details' } }, { path: 'book-create', component: BookCreateComponent, data: { title: 'Create Book' } }, { path: '', redirectTo: '/books', pathMatch: 'full' } ]; --------------------------------------------- 9.2. Add 'book-create' link ------- Edit `src/app/book/book.component.html` ------- <h1>Book List <a [routerLink]="['/book-create']" class="btn btn-default btn-lg"> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> </a> </h1> --------------------------------------------- 9.3. Update 'book-create' component ts ------- Edit and replace `src/app/book/book-create.component.ts` ------- import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-book-create', templateUrl: './book-create.component.html', styleUrls: ['./book-create.component.css'], encapsulation: ViewEncapsulation.None }) export class BookCreateComponent implements OnInit { book = {}; constructor(private http: HttpClient, private router: Router) { } ngOnInit() { } saveBook() { this.http.post('/book', this.book).subscribe( res => { let id = res['_id']; this.router.navigate(['/book-details', id]); }, (err) => { console.log(err); } ); } } --------------------------------------------- 9.4. Update 'book-create' component html ------- Edit and replace `src/app/book-create/book-create.component.html` ------ <div class="container"> <h1>Add New Book</h1> <div class="row"> <div class="col-md-6"> <form (ngSubmit)="saveBook()" #bookForm="ngForm"> <div class="form-group"> <label for="name">ISBN</label> <input type="text" class="form-control" [(ngModel)]="book.isbn" name="isbn" required> </div> <div class="form-group"> <label for="name">Title</label> <input type="text" class="form-control" [(ngModel)]="book.title" name="title" required> </div> <div class="form-group"> <label for="name">Author</label> <input type="text" class="form-control" [(ngModel)]="book.author" name="author" required> </div> <div class="form-group"> <label for="name">Published Year</label> <input type="number" class="form-control" [(ngModel)]="book.published_year" name="published_year" required> </div> <div class="form-group"> <label for="name">Publisher</label> <input type="text" class="form-control" [(ngModel)]="book.publisher" name="publisher" required> </div> <div class="form-group"> <button type="submit" class="btn btn-success" [disabled]="!bookForm.form.valid">Save</button> </div> </form> </div> </div> </div> --------------------------------------------- 10. Create Angular 5 Component for Edit Book ng g component book-edit 10.1. Update router ------- Add route to `src/app/app.module.ts` ------- const appRoutes: Routes = [ { path: 'books', component: BookComponent, data: { title: 'Book List' } }, { path: 'book-details/:id', component: BookDetailComponent, data: { title: 'Book Details' } }, { path: 'book-create', component: BookCreateComponent, data: { title: 'Create Book' } }, { path: 'book-edit/:id', component: BookEditComponent, data: { title: 'Edit Book' } }, { path: '', redirectTo: '/books', pathMatch: 'full' } ]; --------------------------------------------- 10.2. Update book-details component html ------- Edit `src/app/book-details/book-details.component.html` --------- <div class="row"> <div class="col-md-12"> <a [routerLink]="['/book-edit', book._id]" class="btn btn-success">EDIT</a> </div> </div> --------------------------------------------- 10.3. Update book-edit component ts ------- Edit and replace `src/app/book-edit/book-edit.component.ts` -------- import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-book-edit', templateUrl: './book-edit.component.html', styleUrls: ['./book-edit.component.css'], encapsulation: ViewEncapsulation.None }) export class BookEditComponent implements OnInit { book = {}; constructor(private http: HttpClient, private router: Router, private route: ActivatedRoute) { } ngOnInit() { this.getBook(this.route.snapshot.params['id']); } getBook(id) { this.http.get('/book/'+id).subscribe(data => { this.book = data; }); } updateBook(id, data) { this.http.put('/book/'+id, data).subscribe( res => { let id = res['_id']; this.router.navigate(['/book-details', id]); }, (err) => { console.log(err); } ); } } --------------------------------------------- 10.4. Update book-edit component html ------- Edit and replace `src/app/book-edit/book-edit.component.html` -------- <div class="container"> <h1>Edit Book</h1> <div class="row"> <div class="col-md-6"> <form (ngSubmit)="updateBook(book._id)" #bookForm="ngForm"> <div class="form-group"> <label for="name">ISBN</label> <input type="text" class="form-control" [(ngModel)]="book.isbn" name="isbn" required> </div> <div class="form-group"> <label for="name">Title</label> <input type="text" class="form-control" [(ngModel)]="book.title" name="title" required> </div> <div class="form-group"> <label for="name">Author</label> <input type="text" class="form-control" [(ngModel)]="book.author" name="author" required> </div> <div class="form-group"> <label for="name">Published Year</label> <input type="number" class="form-control" [(ngModel)]="book.published_year" name="published_year" required> </div> <div class="form-group"> <label for="name">Publisher</label> <input type="text" class="form-control" [(ngModel)]="book.publisher" name="publisher" required> </div> <div class="form-group"> <button type="submit" class="btn btn-success" [disabled]="!bookForm.form.valid">Update</button> </div> </form> </div> </div> </div> --------------------------------------------- 11. Create Delete Function on Book-Detail Component // Open and edit `src/app/book-detail/book-detail`.component.ts then add `Router` module to `@angular/router`. import { ActivatedRoute, Router } from '@angular/router'; // Inject `Router` in the constructor params. constructor(private router: Router, private route: ActivatedRoute, private http: HttpClient) { } // Add this function for delete book. ------------- code starts ------------- deleteBook(id) { this.http.delete('/book/'+id) .subscribe(res => { this.router.navigate(['/books']); }, (err) => { console.log(err); } ); } ------------- code ends ------------- // Add delete button in `src/app/book-detail/book-detail.component.html` on the right of Edit routerLink. ------------- code starts ------------- <div class="row"> <div class="col-md-12"> <a [routerLink]="['/book-edit', book._id]" class="btn btn-success">EDIT</a> <button class="btn btn-danger" type="button" (click)="deleteBook(book._id)">DELETE</button> </div> </div> ------------- code ends ------------- 12. Run and test your CRUD app (-: npm start
Exercise
- Add link 'Show Details' to list of books
- Make your jewel app working as restful CRUD mean stack (-: