MEAN Stack Exercises

From Training Material
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.


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

  1. Add link 'Show Details' to list of books
  2. Make your jewel app working as restful CRUD mean stack (-: