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 (-: