1582771480
A comprehensive step by step tutorial means starting to build complete MEAN stack Angular 9 realtime CRUD web app from scratch to fully functional web app. Realtime CRUD web app is the create, read, update, and delete operation in realtime response. Every change or new data that created will automatically read by the client without reloading or refresh the browser or re-initiate component in Angular 9. This time we will build a data entry for item sales which response as a realtime list and pie chart.
As you see in the steps list, we will use Socket.io for realtime operation in both server and client. Also, using Ng2Charts and Chart.js to display a pie chart on the list page. The following tools, frameworks, modules, and libraries are required for this tutorial:
Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.
node -v
v12.16.1
npm -v
6.13.7
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. To create the Express.js app, we will be using the Express generator. Type this command to install it.
sudo npm install -g express-generator
Next, create an Express.js app by typing this command.
express sales-report --no-view
Go to the newly created sales-report folder then install all NPM modules.
cd ./sales-report
npm install
Now, we have this Express.js app structure for the sales-report app.
.
|-- app.js
|-- bin
| `-- www
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
| |-- images
| |-- index.html
| |-- javascripts
| `-- stylesheets
| `-- style.css
`-- routes
|-- index.js
`-- users.js
To check and sanitize the Express.js app, run this app for the first time.
nodemon
or
npm start
Then you will see this page when open the browser and go to localhost:3000
.
To make this Express.js server accessible from the different port or domain. Enable the CORS by adding this module.
npm i --save cors
Next, add this require after other require
.
var cors = require('cors');
Then add this line to app.js
before other app.use
.
app.use(cors());
We will use Mongoose as the ODM for MongoDB. For realtime event emitter, we are using Socket.io. To install those required modules, type this command.
npm install --save mongoose socketio socket.io-client
Next, open and edit app.js
then declare the Mongoose module.
var mongoose = require('mongoose');
Create a connection to the MongoDB server using these lines of codes.
mongoose.connect('mongodb://localhost/blog-cms', {
promiseLibrary: require('bluebird'),
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
}).then(() => console.log('connection successful'))
.catch((err) => console.error(err));
Now, if you re-run again Express.js server after running MongoDB server or daemon, you will see this information in the console.
[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`
connection successful
That’s mean, the connection to the MongoDB is successful.
We will use MongoDB collections for Sales Report. For that, we need to create new Mongoose models or schemas for it. First, create a new folder in the root of the project folder that holds the Mongoose models or schemas files then add this model file.
mkdir models
touch models/Sales.js
Open and edit models/Sales.js
then add these lines of Mongoose schema.
var mongoose = require('mongoose');
var SalesSchema = new mongoose.Schema({
id: String,
itemId: String,
itemName: String,
itemPrice: Number,
itemQty: Number,
toralPrice: String,
updated: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Sales', SalesSchema);
We call this Express router as the realtime router because every new data posted the Socket.io will emit that new data to the client then the client will get the list of data. First, create a new file inside the routes
folder.
touch routes/sales.js
Open and edit routes/sales.js
then add these imports.
var express = require('express');
var router = express.Router();
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var Sales = require('../models/Sales.js');
Add a server port for Socket.io to listen to requests from the client.
server.listen(4000)
Add these implementations of the Socket.io in the Express router.
// socket io
io.on('connection', function (socket) {
socket.on('newdata', function (data) {
io.emit('new-data', { data: data });
});
socket.on('updatedata', function (data) {
io.emit('update-data', { data: data });
});
});
Add a router to get a list of data from MongoDB collection.
// list data
router.get('/', function(req, res) {
Sales.find(function (err, sales) {
if (err) return next(err);
res.json(sales);
});
});
Add a router to get the sales report data from MongoDB collection.
// item sales report
router.get('/itemsales', function(req, res, next) {
Sales.aggregate([
{
$group: {
_id: { itemId: '$itemId', itemName: '$itemName' },
totalPrice: {
$sum: '$totalPrice'
}
}
},
{ $sort: {totalPrice: 1} }
], function (err, sales) {
if (err) return next(err);
res.json(sales);
});
});
Add other routers of CRUD operational.
// get data by id
router.get('/:id', function(req, res, next) {
Sales.findById(req.params.id, function (err, sales) {
if (err) return next(err);
res.json(sales);
});
});
// post data
router.post('/', function(req, res, next) {
Sales.create(req.body, function (err, sales) {
if (err) {
console.log(err);
return next(err);
}
res.json(sales);
});
});
// put data
router.put('/:id', function(req, res, next) {
Sales.findByIdAndUpdate(req.params.id, req.body, function (err, sales) {
if (err) {
console.log(err);
return next(err);
}
res.json(sales);
});
});
// delete data by id
router.delete('/:id', function(req, res, next) {
Sales.findByIdAndRemove(req.params.id, req.body, function (err, sales) {
if (err) return next(err);
res.json(sales);
});
});
Run again the MongoDB and Express server then open a new terminal tab or command line to test the Restful API using the CURL command. To check to get the list of data types this CURL command.
curl -i -H "Accept: application/json" localhost:3000/api
That command should show the response of the empty JSON array.
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 8142
ETag: W/"1fce-/FHcYzQEtYsn2ig1EPBAVYQ+2eE"
Date: Fri, 21 Feb 2020 02:19:15 GMT
Connection: keep-alive
[]
Now, let’s post a data to the REST API using this command.
curl -i -X POST -H "Content-Type: application/json" -d '{ "itemId": "10011", "itemName": "Nokia 8", "itemPrice": 388, "itemQty": 5, "totalPrice": 1940 }' localhost:3000/api
That command should show a response like this.
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 165
ETag: W/"a5-ssGlVsvJfkHmQ/nh8cZsfE2Yca4"
Date: Fri, 21 Feb 2020 02:25:24 GMT
Connection: keep-alive
{"_id":"5e4f3f94f1bcd5268893a5d8","itemId":"10011","itemName":"Nokia 8","itemPrice":388,"itemQty":5,"totalPrice":"1940","updated":"2020-02-21T02:25:24.705Z","__v":0}
We will use Angular 9 CLI to install the Angular 9 app. Just type this command to install or update the Angular 9 CLI.
sudo npm install -g @angular/cli
Still in this project folder, create a new Angular 9 app by running this command.
ng new client
If you get the question like below, choose Yes
and SCSS
(or whatever you like to choose).
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/
documentation/syntax#scss ]
Next, go to the newly created Angular 9 project folder.
cd client
Type this command to run the Angular 9 app for the first time.
ng serve --open
Using the “–open” parameter will automatically open this Angular 9 app in the default browser. Now, the Angular initial app looks like this.
In the previous step, we have added Angular routing for this app. Next, we just add the required Angular components for this sales-report app. Just type these commands to generate them.
ng g component sales
ng g component sales-details
ng g component add-sales
ng g component edit-sales
We don’t need to add or register those components to the app.module.ts because it already added automatically. Next, open and edit src/app/app-routing.module.ts
then add these imports.
import { SalesComponent } from './sales/sales.component';
import { SalesDetailsComponent } from './sales-details/sales-details.component';
import { AddSalesComponent } from './add-sales/add-sales.component';
import { EditSalesComponent } from './edit-sales/edit-sales.component';
Add these arrays to the existing routes constant that contain route for above-added components.
const routes: Routes = [
{
path: 'sales',
component: SalesComponent,
data: { title: 'List of Sales' }
},
{
path: 'sales-details/:id',
component: SalesDetailsComponent,
data: { title: 'Sales Details' }
},
{
path: 'add-sales',
component: AddSalesComponent,
data: { title: 'Add Sales' }
},
{
path: 'edit-sales/:id',
component: EditSalesComponent,
data: { title: 'Edit Sales' }
},
{ path: '',
redirectTo: '/sales',
pathMatch: 'full'
}
];
Open and edit src/app/app.component.html
and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page.
<div class="container">
<router-outlet></router-outlet>
</div>
Open and edit src/app/app.component.scss
then replace all SASS codes with this.
.container {
padding: 20px;
}
We use Angular 9 service to access the Node-Express REST API for all CRUD operations. The response from the REST API emitted by Observable that can subscribe and read from the Components. Before creating a service for REST API access, first, we have to install or register HttpClientModule
. Open and edit src/app/app.module.ts
then add these imports of FormsModule, ReactiveFormsModule (@angular/forms) and HttpClientModule (@angular/common/http).
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
Add it to @NgModule
imports after BrowserModule
.
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
AppRoutingModule
],
We will use the type specifier to get a typed result object. For that, create a new Typescript file src/app/sales.ts
then add these lines of Typescript codes.
export class Sales {
id: string;
itemId: string;
itemName: string;
itemPrice: number;
itemQty: number;
totalPrice: number;
updated: Date;
}
And create a new Typescript file src/app/chart.ts
then add these lines of Typescript codes.
export class Chart {
id: any;
totalPrice: number;
}
Next, generate an Angular 9 service by typing this command.
ng g service api
Next, open and edit src/app/api.service.ts
then add these imports.
import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
import { Sales } from './sales';
import { Chart } from './chart';
Add these constants before the @Injectable
.
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
const apiUrl = 'http://localhost:3000/api/';
Inject the HttpClient
module to the constructor.
constructor(private http: HttpClient) { }
Add the error handler function that returns as an Observable.
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error);
return of(result as T);
};
}
Add the functions for all CRUD (create, read, update, delete) REST API call of cases and statistic data.
getSales(): Observable<Sales[]> {
return this.http.get<Sales[]>(`${apiUrl}`)
.pipe(
tap(sales => console.log('fetched sales')),
catchError(this.handleError('getSales', []))
);
}
getSalesById(id: string): Observable<Sales> {
const url = `${apiUrl}/${id}`;
return this.http.get<Sales>(url).pipe(
tap(_ => console.log(`fetched sales id=${id}`)),
catchError(this.handleError<Sales>(`getSalesById id=${id}`))
);
}
addSales(sales: Sales): Observable<Sales> {
return this.http.post<Sales>(apiUrl, sales, httpOptions).pipe(
tap((s: Sales) => console.log(`added sales w/ id=${s._id}`)),
catchError(this.handleError<Sales>('addSales'))
);
}
updateSales(id: string, sales: Sales): Observable<any> {
const url = `${apiUrl}/${id}`;
return this.http.put(url, sales, httpOptions).pipe(
tap(_ => console.log(`updated sales id=${id}`)),
catchError(this.handleError<any>('updateSales'))
);
}
deleteSales(id: string): Observable<Sales> {
const url = `${apiUrl}/${id}`;
return this.http.delete<Sales>(url, httpOptions).pipe(
tap(_ => console.log(`deleted sales id=${id}`)),
catchError(this.handleError<Sales>('deleteSales'))
);
}
getChart(): Observable<Chart> {
const url = `${apiUrl}/itemsales`;
return this.http.get<Chart>(url).pipe(
tap(_ => console.log(`fetched chart data`)),
catchError(this.handleError<Chart>(`getChart data`))
);
}
We will show a realtime Angular Material list on one page with the pie chart. For that, we need to prepare Angular Material, Ng2Charts, and Chart.js. Type this Angular schematic command to install Angular Material (@angular/material) in the client folder.
ng add @angular/material
If there are questions like below, just use the default and “Yes” answer.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: http
s://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
We will register all required Angular Material components or modules to src/app/app.module.ts
. Open and edit that file then add these imports of required Angular Material Components.
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
Register the above modules to @NgModule
imports.
imports: [
...
MatInputModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatSortModule,
MatTableModule,
MatIconModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule
],
Next, install Ng2Charts (ng2-charts), Chart.js and @types/socket.io-client by type this command.
npm i --save ng2-charts chart.js chartjs-plugin-datalabels @types/socket.io-client
Next, back to src/app/app.module.ts
then add this import of ng2-charts.
import { ChartsModule } from 'ng2-charts';
Add this module to the @NgModule imports.
imports: [
...
ChartsModule
],
To make socket-io.client working with Angular 9 and Typescript, open and edit client/src/polyfills.ts
then add this line at the bottom of the file.
(window as any).global = window;
Next, open and edit src/app/sales/sales.component.ts
then add these imports.
import * as io from 'socket.io-client';
import { ChartType, ChartOptions } from 'chart.js';
import { Label } from 'ng2-charts';
import { ApiService } from '../api.service';
import { Sales } from '../sales';
import { Chart } from '../chart';
Declare the socket.io variable that listens to port 4000.
socket = io('http://localhost:4000');
Declare Chart data that hold the array of chart data from the REST API response.
chartData: Chart[] = [];
Declare the required Pie Chart elements as the variables;
public pieChartOptions: ChartOptions = {
responsive: true,
legend: {
position: 'top',
},
plugins: {
datalabels: {
formatter: (value, ctx) => {
const label = ctx.chart.data.labels[ctx.dataIndex];
return label;
},
},
}
};
public pieChartLabels: Label[] = [];
public pieChartData: number[] = [];
public pieChartType: ChartType = 'pie';
public pieChartLegend = true;
public pieChartPlugins = [pluginDataLabels];
public pieChartColors = [];
Declare the required variables to build an Angular Material table.
displayedColumns: string[] = ['itemId', 'itemName', 'totalPrice'];
data: Sales[] = [];
isLoadingResults = true;
Inject the Angular ApiService to the constructor.
constructor(private api: ApiService) { }
Add a function to get sales data from the REST API.
getSales() {
this.api.getSales()
.subscribe((res: any) => {
this.data = res;
console.log(this.data);
this.isLoadingResults = false;
}, err => {
console.log(err);
this.isLoadingResults = false;
});
}
Add a function to get chart data from the REST API then implement it to the ng2-charts/Chart.js pie chart.
getChartData() {
this.api.getChart()
.subscribe((res: any) => {
console.log(res);
this.chartData = res;
this.pieChartLabels = [];
this.pieChartData = [];
this.pieChartColors = [];
const backgrounds = [];
this.chartData.forEach((ch, idx) => {
this.pieChartLabels.push(ch._id.itemName);
this.pieChartData.push(ch.totalPrice);
backgrounds.push(`rgba(${0 + (idx * 10)}, ${255 - (idx * 20)}, ${0 + (idx * 10)}, 0.3)`);
});
this.pieChartColors = [
{
backgroundColor: backgrounds
}
];
}, err => {
console.log(err);
});
}
Call those functions inside Angular 9 NgOnInit function including the socket.io listener that listens the data change the reload the sales and chart data.
ngOnInit(): void {
this.getSales();
this.getChartData();
this.socket.on('update-data', function(data: any) {
this.getSales();
this.getChartData();
}.bind(this));
}
Next, open and edit src/app/sales/sales.component.html
then replace all HTML tags with these.
<div class="example-container mat-elevation-z8">
<h2>Sales List</h2>
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/add-sales']"><mat-icon>add</mat-icon> Sales</a>
</div>
<div class="top-charts">
<div class="chart">
<canvas baseChart
[data]="pieChartData"
[labels]="pieChartLabels"
[chartType]="pieChartType"
[options]="pieChartOptions"
[plugins]="pieChartPlugins"
[colors]="pieChartColors"
[legend]="pieChartLegend">
</canvas>
</div>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="data" class="example-table"
matSort matSortActive="itemName" matSortDisableClear matSortDirection="asc">
<!-- Item ID Column -->
<ng-container matColumnDef="itemId">
<th mat-header-cell *matHeaderCellDef>Item ID</th>
<td mat-cell *matCellDef="let row">{{row.itemId}}</td>
</ng-container>
<!-- Item Name Column -->
<ng-container matColumnDef="itemName">
<th mat-header-cell *matHeaderCellDef>Item Name</th>
<td mat-cell *matCellDef="let row">{{row.itemName}}</td>
</ng-container>
<!-- Total Price Column -->
<ng-container matColumnDef="totalPrice">
<th mat-header-cell *matHeaderCellDef>Total Price</th>
<td mat-cell *matCellDef="let row">{{row.totalPrice}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/sales-details/', row._id]"></tr>
</table>
</div>
</div>
As you see, we put the ng2-charts/Chart.js pie chart a the top of the sales table. Next, open and edit src/app/sales/sales.component.scss
the add these SCSS codes.
/* Structure */
.example-container {
position: relative;
padding: 5px;
}
.example-table-container {
position: relative;
max-height: 400px;
overflow: auto;
}
table {
width: 100%;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.example-rate-limit-reached {
color: #980000;
max-width: 360px;
text-align: center;
}
/* Column Widths */
.mat-column-number,
.mat-column-state {
max-width: 64px;
}
.mat-column-created {
max-width: 124px;
}
.mat-flat-button {
margin: 5px;
}
.top-charts {
margin: 20px;
padding: 20px;
}
We will show realtime data details using Angular Material and Socket.io. This component listens the Socket.io for any data change then reloads the data. Also, we will add a button to delete data that emit the update status to the Socket.io after successfully delete. Next, open and edit src/app/sales-details/sales-details.component.ts
then add these lines of imports.
import * as io from 'socket.io-client';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Sales } from './../sales';
Inject the above modules to the constructor.
constructor(private route: ActivatedRoute, private api: ApiService, private router: Router) { }
Declare the variables before the constructor for hold sales data that get from the API and socket.io variable that listen to the port 4000.
socket = io('http://localhost:4000');
sales: Sales = { _id: '', itemId: '', itemName: '', itemPrice: null, itemQty: null, totalPrice: null, updated: null };
isLoadingResults = true;
Add a function for getting sales data from the API.
getSalesDetails(id: string) {
this.api.getSalesById(id)
.subscribe((data: any) => {
this.sales = data;
console.log(this.sales);
this.isLoadingResults = false;
});
}
Call that function when the component is initiated. Also, add the socket.io listener to listen to the data changes.
ngOnInit(): void {
this.getSalesDetails(this.route.snapshot.params.id);
this.socket.on('update-data', function(data: any) {
this.getSalesDetails();
}.bind(this));
}
Add this function to delete a sales then emit a message to the socket.io.
deleteSales(id: any) {
this.isLoadingResults = true;
this.api.deleteSales(id)
.subscribe(res => {
this.isLoadingResults = false;
this.router.navigate(['/']);
this.socket.emit('updatedata', res);
}, (err) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
For the view, open and edit src/app/sales-details/sales-details.component.html
then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<h2>Sales Cases Details</h2>
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<mat-card-header>
<mat-card-title><h2>{{sales.itemName}}</h2></mat-card-title>
</mat-card-header>
<mat-card-content>
<dl>
<dt>Item ID:</dt>
<dd>{{sales.itemId}}</dd>
<dt>Price:</dt>
<dd>{{sales.itemPrice}}</dd>
<dt>Qty:</dt>
<dd>{{sales.itemQty}}</dd>
<dt>Total Price:</dt>
<dd>{{sales.totalPrice}}</dd>
<dt>Update Date:</dt>
<dd>{{sales.updated | date}}</dd>
</dl>
</mat-card-content>
<mat-card-actions>
<a mat-flat-button color="primary" [routerLink]="['/edit-sales', sales._id]"><mat-icon>edit</mat-icon> Sales</a>
<a mat-flat-button color="warn" (click)="deleteSales(sales._id)"><mat-icon>delete</mat-icon> Sales</a>
</mat-card-actions>
</mat-card>
</div>
Finally, open and edit src/app/sales-details/sales-details.component.scss
then add this lines of SCSS codes.
/* Structure */
.example-container {
position: relative;
padding: 5px;
}
.example-loading-shade {
position: absolute;
top: 0;
left: 0;
bottom: 56px;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.mat-flat-button {
margin: 5px;
}
To add new sales data, we will use an Angular Material 9 Reactive Forms. After data POST to REST API successfully then it emits a message to the socket.io. Open and edit src/app/add-sales/add-sales.component.ts
then add these imports.
import * as io from 'socket.io-client';
import { Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
Create a new class before the main class @Components
that implementing Angular ErrorStateMatcher.
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
Inject the imported modules to the constructor of the main class.
constructor(private router: Router, private api: ApiService, private formBuilder: FormBuilder) { }
Declare variables for socket.io and the Form Group and all of the required fields inside the form before the constructor.
socket = io('http://localhost:4000');
salesForm: FormGroup;
itemId = '';
itemName = '';
itemPrice: number = null;
itemQty: number = null;
totalPrice: number = null;
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
Add initial validation for each field.
ngOnInit(): void {
this.salesForm = this.formBuilder.group({
itemId : [null, Validators.required],
itemName : [null, Validators.required],
itemPrice : [null, Validators.required],
itemQty : [null, Validators.required],
totalPrice : [null, Validators.required]
});
}
Create a function for submitting or POST sales form and emit a message to socket.io after successfully submit data.
onFormSubmit() {
this.isLoadingResults = true;
this.api.addSales(this.salesForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.socket.emit('updatedata', res);
this.router.navigate(['/sales-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
});
}
Next, open and edit src/app/add-sales/add-sales
then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<h2>Add Sales</h2>
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="salesForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<mat-label>Item ID</mat-label>
<input matInput placeholder="Item ID" formControlName="itemId"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemId').valid && salesForm.get('itemId').touched">Please enter Item ID</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Name</mat-label>
<input matInput placeholder="Item Name" formControlName="itemName"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemName').valid && salesForm.get('itemName').touched">Please enter Item Name</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Price</mat-label>
<input matInput type="number" placeholder="Item Price" formControlName="itemPrice"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemPrice').valid && salesForm.get('itemPrice').touched">Please enter Item Price</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Qty</mat-label>
<input matInput type="number" placeholder="Item Qty" formControlName="itemQty"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemQty').valid && salesForm.get('itemQty').touched">Please enter Item Qty</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Total Price</mat-label>
<input matInput type="number" placeholder="Total Price" formControlName="totalPrice"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('totalPrice').valid && salesForm.get('totalPrice').touched">Please enter Total Price</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!salesForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Finally, open and edit src/app/add-sales/add-sales.component.scss
then add this SCSS codes.
/* Structure */
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child(0) {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
We already put an edit button inside the Cases Details component for the call Edit page. Now, open and edit src/app/edit-sales/edit-sales.component.ts
then add these imports.
import * as io from 'socket.io-client';
import { Router, ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
Create a new class before the main class @Components
.
/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}
Inject the imported modules to the constructor of the main class.
constructor(private router: Router, private route: ActivatedRoute, private api: ApiService, private formBuilder: FormBuilder) { }
Declare the socket.io and Form Group variable and all of the required variables for the cases-form before the constructor.
socket = io('http://localhost:4000');
salesForm: FormGroup;
_id = '';
itemId = '';
itemName = '';
itemPrice: number = null;
itemQty: number = null;
totalPrice: number = null;
isLoadingResults = false;
matcher = new MyErrorStateMatcher();
Next, add validation for all fields when the component is initiated.
ngOnInit(): void {
this.getSalesById(this.route.snapshot.params.id);
this.salesForm = this.formBuilder.group({
itemId : [null, Validators.required],
itemName : [null, Validators.required],
itemPrice : [null, Validators.required],
itemQty : [null, Validators.required],
totalPrice : [null, Validators.required]
});
}
Create a function for getting sales data by the ID that will fill to each form field.
getSalesById(id: any) {
this.api.getSalesById(id).subscribe((data: any) => {
this._id = data._id;
this.salesForm.setValue({
itemId: data.itemId,
itemName: data.itemName,
itemPrice: data.itemPrice,
itemQty: data.itemQty,
totalPrice: data.totalPrice
});
});
}
Create a function to update the sales changes then emit the change message to the socket.io.
onFormSubmit() {
this.isLoadingResults = true;
this.api.updateSales(this._id, this.salesForm.value)
.subscribe((res: any) => {
const id = res._id;
this.isLoadingResults = false;
this.socket.emit('updatedata', res);
this.router.navigate(['/sales-details', id]);
}, (err: any) => {
console.log(err);
this.isLoadingResults = false;
}
);
}
Add a function for handling the show sales details button.
salesDetails() {
this.router.navigate(['/sales-details', this._id]);
}
Next, open and edit src/app/edit-sales/edit-sales.component.html
then replace all HTML tags with this.
<div class="example-container mat-elevation-z8">
<h2>Edit Sales</h2>
<div class="example-loading-shade"
*ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="button-row">
<a mat-flat-button color="primary" [routerLink]="['/']"><mat-icon>list</mat-icon></a>
</div>
<mat-card class="example-card">
<form [formGroup]="salesForm" (ngSubmit)="onFormSubmit()">
<mat-form-field class="example-full-width">
<mat-label>Item ID</mat-label>
<input matInput placeholder="Item ID" formControlName="itemId"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemId').valid && salesForm.get('itemId').touched">Please enter Item ID</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Name</mat-label>
<input matInput placeholder="Item Name" formControlName="itemName"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemName').valid && salesForm.get('itemName').touched">Please enter Item Name</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Price</mat-label>
<input matInput type="number" placeholder="Item Price" formControlName="itemPrice"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemPrice').valid && salesForm.get('itemPrice').touched">Please enter Item Price</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Item Qty</mat-label>
<input matInput type="number" placeholder="Item Qty" formControlName="itemQty"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('itemQty').valid && salesForm.get('itemQty').touched">Please enter Item Qty</span>
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Total Price</mat-label>
<input matInput type="number" placeholder="Total Price" formControlName="totalPrice"
[errorStateMatcher]="matcher">
<mat-error>
<span *ngIf="!salesForm.get('totalPrice').valid && salesForm.get('totalPrice').touched">Please enter Total Price</span>
</mat-error>
</mat-form-field>
<div class="button-row">
<button type="submit" [disabled]="!salesForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
</div>
</form>
</mat-card>
</div>
Finally, open and edit src/app/edit-sales/edit-sales.component.scss
then add these lines of SCSS codes.
/* Structure */
.example-container {
position: relative;
padding: 5px;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.example-full-width:nth-last-child(0) {
margin-bottom: 10px;
}
.button-row {
margin: 10px 0;
}
.mat-flat-button {
margin: 5px;
}
Now, it’s a time to run the MEAN stack Angular 9 app together. First, run the MongoDB server in another terminal tab.
mongod
Open a new terminal tab then open the mongo command.
mongo
Go to the sales-report database then import these initial data.
use sales-report
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 2, totalPrice: 1996, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 3, totalPrice: 2994, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10001', itemName: 'Samsung Galaxy S10+', itemPrice: 998, itemQty: 1, totalPrice: 998, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 2, totalPrice: 2198, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-13')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-14')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 3, totalPrice: 3297, updated: new Date('2020-02-15')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-16')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-17')});
db.sales.insert({itemId: '10002', itemName: 'Samsung Note 10 Lite', itemPrice: 1099, itemQty: 1, totalPrice: 1099, updated: new Date('2020-02-18')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 2, totalPrice: 998, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 3, totalPrice: 1497, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10003', itemName: 'LG G7+ ThinQ', itemPrice: 499, itemQty: 1, totalPrice: 499, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10004', itemName: 'Asus ROG 2 Phone', itemPrice: 1199, itemQty: 1, totalPrice: 1199, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10005', itemName: 'Samsung Galaxy Fold', itemPrice: 1299, itemQty: 1, totalPrice: 1299, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10005', itemName: 'Samsung Galaxy Fold', itemPrice: 1299, itemQty: 1, totalPrice: 1299, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 2, totalPrice: 2798, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 3, totalPrice: 4197, updated: new Date('2020-02-13')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-14')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-15')});
db.sales.insert({itemId: '10006', itemName: 'Google Pixel 4 XL', itemPrice: 1399, itemQty: 1, totalPrice: 1399, updated: new Date('2020-02-16')});
db.sales.insert({itemId: '10007', itemName: 'iPhone 11 Pro', itemPrice: 1499, itemQty: 2, totalPrice: 2998, updated: new Date('2020-02-17')});
db.sales.insert({itemId: '10007', itemName: 'iPhone 11 Pro', itemPrice: 1499, itemQty: 1, totalPrice: 1499, updated: new Date('2020-02-18')});
db.sales.insert({itemId: '10008', itemName: 'iPhone 11', itemPrice: 899, itemQty: 3, totalPrice: 2697, updated: new Date('2020-02-01')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-02')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-03')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-04')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-05')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-06')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-07')});
db.sales.insert({itemId: '10009', itemName: 'iPhone 11', itemPrice: 899, itemQty: 1, totalPrice: 899, updated: new Date('2020-02-08')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-09')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-10')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-11')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-12')});
db.sales.insert({itemId: '10010', itemName: 'iPhone 9', itemPrice: 399, itemQty: 1, totalPrice: 399, updated: new Date('2020-02-13')});
Next, run the Node-Express REST API server at the root of this project.
nodemon
Then run the Angular 9 app on the other terminal tab inside the client folder.
cd client
ng serve --open
And here they are the full MEAN stack with Angular 9 in the browser.
That it’s, the MEAN Stack Angular 9 Build Realtime CRUD Web App Quickly. you can get the full source code on our GitHub.
If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.
Thanks!
#angular #mean #mongodb #nodejs #express
1595334123
I consider myself an active StackOverflow user, despite my activity tends to vary depending on my daily workload. I enjoy answering questions with angular tag and I always try to create some working example to prove correctness of my answers.
To create angular demo I usually use either plunker or stackblitz or even jsfiddle. I like all of them but when I run into some errors I want to have a little bit more usable tool to undestand what’s going on.
Many people who ask questions on stackoverflow don’t want to isolate the problem and prepare minimal reproduction so they usually post all code to their questions on SO. They also tend to be not accurate and make a lot of mistakes in template syntax. To not waste a lot of time investigating where the error comes from I tried to create a tool that will help me to quickly find what causes the problem.
Angular demo runner
Online angular editor for building demo.
ng-run.com
<>
Let me show what I mean…
There are template parser errors that can be easy catched by stackblitz
It gives me some information but I want the error to be highlighted
#mean stack #angular 6 passport authentication #authentication in mean stack #full stack authentication #mean stack example application #mean stack login and registration angular 8 #mean stack login and registration angular 9 #mean stack tutorial #mean stack tutorial 2019 #passport.js
1621426329
AppClues Infotech is one of the leading Enterprise Angular Web Apps Development Company in USA. Our dedicated & highly experienced Angular app developers build top-grade Angular apps for your business with immersive technology & superior functionalities.
For more info:
Website: https://www.appcluesinfotech.com/
Email: info@appcluesinfotech.com
Call: +1-978-309-9910
#top enterprise angular web apps development company in usa #enterprise angular web apps development #hire enterprise angular web apps developers #best enterprise angular web app services #custom enterprise angular web apps solution #professional enterprise angular web apps developers
1595059664
With more of us using smartphones, the popularity of mobile applications has exploded. In the digital era, the number of people looking for products and services online is growing rapidly. Smartphone owners look for mobile applications that give them quick access to companies’ products and services. As a result, mobile apps provide customers with a lot of benefits in just one device.
Likewise, companies use mobile apps to increase customer loyalty and improve their services. Mobile Developers are in high demand as companies use apps not only to create brand awareness but also to gather information. For that reason, mobile apps are used as tools to collect valuable data from customers to help companies improve their offer.
There are many types of mobile applications, each with its own advantages. For example, native apps perform better, while web apps don’t need to be customized for the platform or operating system (OS). Likewise, hybrid apps provide users with comfortable user experience. However, you may be wondering how long it takes to develop an app.
To give you an idea of how long the app development process takes, here’s a short guide.
_Average time spent: two to five weeks _
This is the initial stage and a crucial step in setting the project in the right direction. In this stage, you brainstorm ideas and select the best one. Apart from that, you’ll need to do some research to see if your idea is viable. Remember that coming up with an idea is easy; the hard part is to make it a reality.
All your ideas may seem viable, but you still have to run some tests to keep it as real as possible. For that reason, when Web Developers are building a web app, they analyze the available ideas to see which one is the best match for the targeted audience.
Targeting the right audience is crucial when you are developing an app. It saves time when shaping the app in the right direction as you have a clear set of objectives. Likewise, analyzing how the app affects the market is essential. During the research process, App Developers must gather information about potential competitors and threats. This helps the app owners develop strategies to tackle difficulties that come up after the launch.
The research process can take several weeks, but it determines how successful your app can be. For that reason, you must take your time to know all the weaknesses and strengths of the competitors, possible app strategies, and targeted audience.
The outcomes of this stage are app prototypes and the minimum feasible product.
#android app #frontend #ios app #minimum viable product (mvp) #mobile app development #web development #android app development #app development #app development for ios and android #app development process #ios and android app development #ios app development #stages in app development
1619170894
Looking for the best custom AngularJS app development company? AppClues Infotech is a top-rated AngularJS app development company in USA producing robust, highly interactive and data-driven AngularJS web and mobile applications with advanced features & technologies.
For more info:
Website: https://www.appcluesinfotech.com/
Email: info@appcluesinfotech.com
Call: +1-978-309-9910
#custom angular js web app development company in usa #best angular js app development company in usa #hire angular js app developers in usa #top angular js app development company #professional angular js app developers #leading angular js app development agency
1596094635
What is MEAN Stack Developer?
MEAN Stack Developer is a programmer who operates on the MEAN stack. He works on the backend plus the front end of the application. They are all JavaScript based and therefore a MEAN developer should have excellent JS knowledge, for which you can join MEAN Stack Online Training Program.
Skillets of MEAN Stack developer
• Knowledge of working on the Front-end and Back-end processes
• Work with HTML & CSS
• Understanding of programming templates and architecture design guidelines
• Knowledge of continuous integration, web development, and cloud technologies
• Excellent understanding of DB architecture
• Knowledge of SDLC and experience developing in an Agile environment
• Collaborate with the IT team to build robust systems to support business objectives
• Hands-on experience on Mongo, Angular, Express, Node.
Future of MEAN stack Developer
Being, a Mean stack developer is a highly desirable, challenging vocation. So, if you are ready to work on the diverse skill set and have the experience of working with various code languages and application, then you will become successful MEAN stack developer.
Scope of MEAN stack developer
MEAN Stack Development is the best career prospect today with unlimited growth and scope. The national Indian median salary was around 76K $ pa according to Glassdoor.com. All you need to do is get cracking on your skill set by joining MEAN Stack course in Delhi, earn your certification and through your job experience and continued experiential learning keep pace with newer iterations in technology.
Developers are available in various process streams like Backend, Frontend, Database, Testing, and Mobile Apps. Older practices were that as you gain experience and expertise you would pursue specialization and progress your career in that direction.
How Can I Start Learning Mean Stack Course Step By Step? Modern best practices have changed the trend.
Skill upgrades and building proficiency in:
• CSS
• HTML
• JavaScript
• Ruby, PHP, or Python which are all-purpose languages.
• Postgres, Oracle, or MySQL, relational-database systems.
• Any web-server which includes Nginx or Apache
• FreeBSD, Ubuntu, or CentOS deployment OS.
• Any system for instance GIT for version-control
By mastering one software technology required for every stack-part you will be in a position to create your own software and use it to add business value at your job.
#mean stack #mean stack training #mean stack certification online #mean stack online course #mean stack online training