MEAN Stack Exercises

From Training Material
Revision as of 12:20, 25 May 2018 by Lsokolowski (talk | contribs) (→‎Exercise)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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

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