- mkdir tsnodeprofiles, start 'code .'
- Create a 'package.json' file.
- Add name, version ...
- http://expressjs.com/ 4.1 npm install express --save 4.2 see the changes of 'package.json' file.
- Create a index.ts file 5.1 copy the 'Hello world example' from http://expressjs.com/en/starter/hello-world.html
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
- npm install typescript --save-dev 6.1 see the changes of 'package.json' file again. 6.2 TypeScript is a superset of JavaScript that compiles to clean JavaScript output. 6.3 http://www.typescriptlang.org/
- 'tsc index.ts', see the file 'index.js'
- 'node index.js' 8.1 check http://localhost:3000/
- ctrl+shift+p, 'ext install typescript'
- ctrl+shift+b to build the project.
- Add 'tsconfig.json', http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
- npm install typings -g
- typings install dt
express -SG 5.1 close the editor and open again, see? the code looks good now. 5.2 import * as express from 'express'; 5.3 compile again? see 'OUTPUT'. 5.4 typings install dtserve-static dtexpress-serve-static-core -SG 5.5 compile again? 5.6 typings install dtmime -SG 5.7 typings search http *5.8 typings i dt~node -SG - 'F5' to run the app. 6.1 see the 'DEBUG CONSOLE' 6.2 check http://localhost:3000/
- Setup breakpoint and debug. *7.1 setup the breakpoint in 'index.js' to have a try. 7.2 'source map', wtf? 7.3 "sourceMap": true (tsconfig.json, launch.json) 7.4 ctrl+shift+b, F5, try again?
- Exclude the files.
8.1 'File -> Preferences -> User Settings 8.2 by setup 'settings.json' in '.vscode' folder with "files.exclude". "/.js": true, "/.map": true, "node_modules": true, "typings": true
- Using template engines with Express, http://expressjs.com/en/guide/using-template-engines.html
- https://handlebarsjs.com/
- https://github.com/ericf/express-handlebars 3.1 npm i express-handlebars --save 3.2 typings i dt~express-handlebars -SG
- Copy the code from 'express-handlebars' website: https://github.com/ericf/express-handlebars 4.1 views/layouts/main.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example App</title>
</head>
<body>
{{{body}}}
</body>
</html>
4.2 views/home.html
<h1>Example App: {{title}}</h1>
- Register view engine
import * as handlebars from 'express-handlebars';
...
// register view engine
app.engine('html', handlebars.create({ defaultLayout: "main", extname: '.html' }).engine);
app.set('view engine', 'html');
- Render home page
res.render('home', { title: 'hello world' });
- Create 'controllers' folder.
- Add 'home.controller.ts' file:
export class HomeController {
index(req, res, next) {
res.render('home', { title: 'hello world' });
}
}
- Change 'index.ts' file, use the 'HomeController':
import { HomeController } from './controllers/home.controller';
var homeController = new HomeController();
...
app.get('/', homeController.index);
- Add 'router.ts' file.
import * as express from 'express';
import { HomeController } from './controllers/home.controller';
export class Router {
router = express.Router();
homeController = new HomeController();
constructor() {
this.router.get('/', this.homeController.index);
}
}
- Change 'index.ts' file, use the 'Router'.
import { Router } from './router';
...
// use router
app.use('/', new Router().router);
- Create a 'public' folder.
- Change 'index.ts' file, add static file support:
app.use(express.static('public'));
- Install jquery and semantic-ui, http://semantic-ui.com/ 3.1 cd public 3.2 https://bower.io/ 3.3 bower install semantic-ui
- Create partial view. 4.1 Create 'partials' folder in the 'views' folder. 4.2 Add a 'head.html' file:
<meta charset="utf-8">
<title>tsnodeprofiles</title>
<link rel="stylesheet" type="text/css" href="/bower_components/semantic/dist/semantic.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script src="/bower_components/semantic/dist/semantic.min.js"></script>
4.3 Change 'main.html' in the layouts folder
<head>
{{> head}}
</head>
- Create 'images' folder in 'public', and copy the 'logo.svg' file.
- Add 'nav.html' in 'partials' folder:
<div class="ui fixed inverted menu">
<div class="main nav">
<a href="/" class="header item">
<img class="logo" src="/images/logo.svg">
InnoCellence
</a>
</div>
</div>
- Change 'main.html' file:
{{> nav}}
<div class="ui main content">
{{{body}}}
</div>
- Create 'styles' folder in 'public'.
- Add 'site.scss' in the 'styles' foler:
.main.content
{
min-height: 480px;
margin-top: 53px;
}
- http://gulpjs.com/
- npm install gulp gulp-watch gulp-scss gulp-run --save-dev
- Copy the 'gulpfile.js' to project root folder.
- gulp
- Change 'head.html', add 'site.css' reference:
<link rel="stylesheet" type="text/css" href="/styles/site.css">
- Hide '.gulp-scss-cache', '.sass-cache' folders.
- Create a 'users.controller.ts' in 'controllers' folder:
export class UsersController {
index(req, res, next) {
res.render('users/index');
}
}
- Create a 'users' folder in 'views' foler.
- Create a 'index.html' file in 'users' folder:
<h1 class="ui header">Users</h1>
- Add router in 'Router.ts':
import { UsersController } from './controllers/users.controller';
...
usersController = new UsersController();
...
this.router.get('/users', this.usersController.index);
- Add a in the 'index.html':
<table class="ui selectable celled table users"> <thead> <tr> <th>User Name</th> <th>Email</th> <th>Location</th> </tr> </thead> <tbody> {{#each users}} <tr> <td>{{FirstName}} {{LastName}}</td> <td>{{Email}}</td> <td>{{Location}}</td> </tr> {{/each }} </tbody> </table>
- Add dummy data:
res.render('users/index', { users: [{ FirstName: 'Johnson', LastName: 'Zhong', Email: '[email protected]', Location: 'Dalian' }] });
- Add a 'New' button:
<div class="row"> <a class="ui primary button" tabindex="0" href="/users/create"> <i class="add icon"></i>New </a> </div>
- Add a 'create' action in the 'UsersCotroller":
create(req, res, next) { res.render('users/edit'); }
- Add a 'edit.html' view in 'views/users/' folder:
<form class="ui form" action='/users/update' method="POST"> <h4 class="ui dividing header">InnoCellence Team Member Profile</h4> <div class="field"> <label>Name</label> <div class="two fields"> <div class="field"> <input type="text" name="firstname" placeholder="First Name" value="{{FirstName}}"/> </div> <div class="field"> <input type="text" name="lastname" placeholder="Last Name" value="{{LastName}}" /> </div> </div> </div> <div class="field"> <label>Discipline</label> <input type="text" name="discipline" placeholder="Discipline" value="{{Discipline}}" /> </div> <div class="two fields"> <div class="field"> <label>Email Address</label> <input type="text" name="email" placeholder="Email Address" value="{{Email}}" /> </div> <div class="field"> <label>Location</label> <select class="ui fluid dropdown" name="location" data-value="{{Location}}"> <option value="">Select Location</option> <option value="Dalian">Dalian</option> <option value="Japan">Japan</option> <option value="Shanghai">Shanghai</option> <option value="Singapore">Singapore</option> </select> </div> </div> <div class="field"> <label>Summary</label> <textarea rows="5" name='summary'>{{Profile.Summary}}</textarea> </div> <div class="field"> <label>Key Skills</label> <textarea rows="2" name='keyskills'></textarea> </div> <div class="field"> <label>Career History</label> <textarea rows="3" name='careerhistories'></textarea> </div> <div class="field"> <label>Education</label> <textarea rows="3" name='educations'></textarea> </div> <div class="field"> <label>Languages</label> <textarea rows="2" name='languages'></textarea> </div> <div class="field"> <label>Certificates and Awards</label> <textarea rows="3" name='certificatesandawards'></textarea> </div> <input type="hidden" name="id" value="{{_id}}" /> <a class="ui button submit" tabindex="0">Submit</a> </form> <script src="/scripts/users/edit.js"></script>
- Add 'edit.ts' in the 'public/scripts/users/' folder:
$(document) .ready(function () { $('select.dropdown').each(function (index, element) { var value = $(element).data('value'); $(element.children).each(function (index, option) { if (option.value == value) { $(option).attr('selected', 'selected'); return false; } }) }) $('select.dropdown').dropdown(); $('.ui.form') .form({ fields: { email: { identifier: 'email', rules: [ { type: 'empty', prompt: 'Please enter your e-mail' }, { type: 'email', prompt: 'Please enter a valid e-mail' } ] } } }); });
- Add '/users/create' in 'Router':
this.router.get('/users/create', this.usersController.create);
- Compile error? 6.1 try 'typings search sematic-ui'? 6.2 ok, let's build our own 'defination' file: 6.2.1 create a 'manual/semantic-ui' folder in 'typings'. 6.2.2 add an 'index.d.ts' file:
interface JQuery { dropdown(): any; form(options: any): any; } interface Element { children: any; value: any; }
6.2.3 add in 'typings/index.d.ts' file:
/// <reference path="manual/semantic-ui/index.d.ts" />
- Add 'update' action in 'UserController':
update(req, res, next) { res.render('users/edit'); }
- Add in 'router.ts' for the post action:
this.router.post('/users/update', this.usersController.update);
- *How to get the post values? 3.1 add 'bodyParser', npm install body-parser --save 3.2 typings install dt~body-parser -SG 3.3 change 'index.ts' file:
import * as bodyParser from 'body-parser'; ... // bodyParser app.use(bodyParser.json({ limit: '1mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '1mb' }));
- https://www.mongodb.com/
- https://mlab.com/
- http://mongoosejs.com/ 3.1 npm install mongoose -save 3.2 typings i dt~mongoose -SG
- Define 'Schema' 4.1 https://github.com/topliceanu/mongoose-gen 4.2 npm install mongoose-gen --save 4.3 Manually create a 'mongoose-gen' dts file:
declare module "mongoose-gen" { function convert(descriptor) : any; }
- Create 'UserProfile' model 1.1 Create a 'models' folder. 1.2 Create a 'user-profile.ts' file 'models' folder:
import * as mongoose from 'mongoose'; import * as mg from 'mongoose-gen'; export class UserProfile { name = "UserProfiles"; default = { Id: '', EmployeeNumber: '', DisplayName: '', FirstName: '', LastName: '', Email: '', Location: '', Role: '', Discipline: '', Thumbnail: '', CreatedAt: '', Profile: { Summary: '', KeySkills: [ '' ], CareerHistories: [ { DateTime: '', Title: '', Company: '', Location: '' } ], Educations: [ { DateTime: '', Degree: '', School: '', Location: '' } ], Languages: [ { Name: '', Level: '' } ], CertificatesAndAwards: [ '' ] } }; schema: mongoose.Schema; model: mongoose.Model<mongoose.Document>; constructor() { this.schema = new mongoose.Schema(mg.convert(this.default)); this.model = mongoose.model(this.name, this.schema); } }
- Create 'Config' file for store the db connection string: 2.1 Create a 'config.ts' file:
export class Config { db = 'mongodb://localhost/tsnodeprofiles'; }
- Create a 'UserProfileService': 3.1 Create a 'services' folder. 3.2 Create a 'user-profile.service.ts' file 'services' folder:
import * as mongoose from 'mongoose'; import { Config } from '../config'; import { UserProfile } from '../models/user-profile'; var config = new Config(); const UserProfileModel = new UserProfile().model; export class UserProfileService { async saveUserProfile(userProfile) { return new Promise<mongoose.Document>(resolve => { mongoose.connect(config.db); if (userProfile.Id) { var id = userProfile.Id; UserProfileModel.findByIdAndUpdate(id, { $set: userProfile }, function (err, res) { resolve(res); }); } else { var userProfileModel = new UserProfileModel(userProfile); userProfileModel.save(function (err, res) { resolve(); }); } }); } }
- Change the 'update' action to save the post data into db.
import { UserProfileService } from '../services/user-profile.service'; const userProfileService = new UserProfileService(); ... update(req, res, next) { var userProfile = { Id: req.body.id, FirstName: req.body.firstname, LastName: req.body.lastname, Email: req.body.email, Location: req.body.location, Discipline: req.body.discipline, Profile: { Summary: req.body.summary } }; userProfileService.saveUserProfile(userProfile) .then(function () { return res.redirect('/users'); }); }
- Add 'getUserProfiles' method in the 'UserProfileService':
constructor() { mongoose.connect(config.db); } ... async getUserProfiles() { return new Promise<any>(resolve => { UserProfileModel.find(function (err, res) { resolve(res); }); }); }
- Change the 'index' action of 'UserController':
index(req, res, next) { userProfileService.getUserProfiles() .then(function (users) { if (users) { res.render('users/index', { users: users }); } }); }
Or
async index(req, res, next) { var users = await userProfileService.getUserProfiles() if (users) { res.render('users/index', { users: users }); } }
- Add 'getUserProfile' method in UserProfileServie:
async getUserProfile(id) { return new Promise<mongoose.Document>(resolve => { UserProfileModel.findById(id, function (err, res) { resolve(res); }); }); }
- Add 'edit' action in UserController:
edit(req, res, next) { var id = req.query.id; userProfileService.getUserProfile(id) .then(function (userProfile) { return res.render('users/edit', userProfile); }); }
Or
async edit(req, res, next) { var id = req.query.id; var userProfile = await userProfileService.getUserProfile(id); return res.render('users/edit', userProfile); }
- Add 'edit' action in Router:
this.router.get('/users/edit', this.usersController.edit);
- Add 'edit' button in the list:
<th>Operations</th> ... <td> <a href='/users/edit?id={{_id}}'><i class="edit icon"></i></a> </td>
- Add 'deleteUserProfile' method in UserProfileService:
async deleteUserProfile(id) { return new Promise<mongoose.Document>(resolve => { UserProfileModel.findByIdAndRemove(id, function (err, res) { resolve(res); }); }); }
- Add 'delete' action in UserController:
delete(req, res, next) { var id = req.query.id; userProfileService.deleteUserProfile(id) .then(function () { return res.redirect('/users'); }); }
Or
async delete(req, res, next) { var id = req.query.id; await userProfileService.deleteUserProfile(id) return res.redirect('/users'); }
- Add 'delete' action in Router:
this.router.get('/users/delete', this.usersController.delete);
- Change list view, add 'delete' button and confirm dialog:
<a href='/users/delete?id={{_id}}' class="delete operation"><i class="delete icon"></i></a> ... <div class="ui dimmer modals page transition"> <div class="ui small modal transition scrolling delete"> <div class="content"> <div class="description"> <p>Are you sure you want to delete this?</p> </div> </div> <div class="actions"> <a class="ui red basic button yes"> <i class="remove icon"></i> Yes </a> <div class="ui green basic button deny"> <i class="undo icon"></i> No </div> </div> </div> </div> <script src="/scripts/users/index.js"></script>
- Add the 'index.js' script file to show the confirm dialog:
$(function () { $('.delete.operation').on('click', function (e) { var href = $(this).attr('href'); $('.delete.modal a.yes.button').attr('href', href); $('.delete.modal').modal('show'); e.preventDefault(); }); })
- Add defination in .d.ts file:
interface JQuery { dropdown(): any; form(options: any): any; modal(options): any; }
- Add 'login' view in the 'views/home/' folder:
<div class="ui middle aligned center aligned grid"> <div class="column"> <h2 class="ui teal image header"> <img src="images/logo.svg" class="image"> <div class="content"> Log-in to your account </div> </h2> <form class="ui large form" action="/login" method="POST"> <div class="ui stacked segment"> <div class="field"> <div class="ui left icon input"> <i class="user icon"></i> <input type="text" name="email" placeholder="E-mail address"> </div> </div> <div class="field"> <div class="ui left icon input"> <i class="lock icon"></i> <input type="password" name="password" placeholder="Password"> </div> </div> <div class="ui fluid large teal submit button">Login</div> </div> <div class="ui error message" {{#if errorMessage}} style="display:block" {{/if}}> <ul class="list"> <li> {{errorMessage}} </li> </ul> </div> </form> </div> </div> <script src="/scripts/home/login.js"></script>
- Add 'login.ts' in the 'public/scripts/home/' folder:
$(document) .ready(function () { $('.ui.form') .form({ fields: { email: { identifier: 'email', rules: [ { type: 'empty', prompt: 'Please enter your e-mail' }, { type: 'email', prompt: 'Please enter a valid e-mail' } ] }, password: { identifier: 'password', rules: [ { type: 'empty', prompt: 'Please enter your password' }, { type: 'length[6]', prompt: 'Your password must be at least 6 characters' } ] } } }); });
- Add a 'clean.html' layout in 'views/layouts/' folder:
<!DOCTYPE html> <html> <head> {{> head}} </head> <body> <div class="ui main container"> {{{body}}} </div> </body> </html>
- Add styles in 'site.scss':
.main.container { .column { max-width: 450px; } }
- Add 'login' action in the HomeController, use the 'clean' layout:
login(req, res, next) { res.render('home/login', { layout: 'clean', errorMessage: '' }); }
- Add '/login' in Router:
this.router.get('/login', this.homeController.login);
- npm install request-promise -save
- typings install dt
request dtrequest-promise dt~form-data -SG - Create a 'office365.service.ts' in the services folder:
import * as rp from 'request-promise'; const endpoint = "https://sginnocellence.sharepoint.com"; const srfUrl = "https://login.microsoftonline.com/extSTS.srf"; const xmlBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:a=\"http://www.w3.org/2005/08/addressing\" xmlns:u=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"><s:Header><a:Action s:mustUnderstand=\"1\">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand=\"1\">https://login.microsoftonline.com/extSTS.srf</a:To><o:Security s:mustUnderstand=\"1\" xmlns:o=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><o:UsernameToken><o:Username>[username]</o:Username><o:Password>[password]</o:Password></o:UsernameToken></o:Security></s:Header><s:Body><t:RequestSecurityToken xmlns:t=\"http://schemas.xmlsoap.org/ws/2005/02/trust\"><wsp:AppliesTo xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\"><a:EndpointReference><a:Address>[endpoint]</a:Address></a:EndpointReference></wsp:AppliesTo><t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType></t:RequestSecurityToken></s:Body></s:Envelope>"; export class Office365Service { async signIn(email, password) { var signInUrl = srfUrl; var requestBody = xmlBody .replace("[username]", email) .replace("[password]", password) .replace("[endpoint]", endpoint); return new Promise<any>(resolve => { rp({ method: 'POST', uri: signInUrl, headers: { 'content_type': "text/xml; charset=utf-8" }, body: requestBody}) .then(function (response) { if (response.indexOf("<wsse:BinarySecurityToken") > 0) { resolve({ success: true, user_id: email }); } else { resolve({ success: false, error: 'Login failed', user_id: email }); } }); }); } }
- Add 'loginPost' in HomeController:
import { Office365Service } from '../services/office365.service'; const office365Service = new Office365Service(); ... loginPost(req, res, next) { var email = req.body.email; var password = req.body.password; office365Service.signIn(email, password) .then(function (result) { if (result.success) { // save auth_token and redirect to home page. req.session.user = { user_id: email } req.session.save(function () { // redirect res.redirect('/'); }); } else { res.render('home/login', { layout: 'clean', errorMessage: result.error }); } }); }
Or
async loginPost(req, res, next) { var email = req.body.email; var password = req.body.password; var result = await office365Service.signIn(email, password); if (result.success) { // save auth_token and redirect to home page. req.session.user = { user_id: email } req.session.save(function () { // redirect res.redirect('/'); }); } else { res.render('home/login', { layout: 'clean', errorMessage: result.error }); } }
- Add '/login' post in Router:
this.router.post('/login', this.homeController.loginPost);
- *Add 'session' and 'cookie' support: 6.1 npm install cookie-parser express-session --save 6.2 typings install dt~form-data -SG 6.3 Use 'session' and 'cookie' in 'index.ts' file:
import * as cookieParser from 'cookie-parser'; import * as session from 'express-session'; import { Config } from './config'; var config = new Config(); ... // use cookie and session app.use(cookieParser(config.session_secret)); app.use(session({ secret: config.session_secret, resave: false, saveUninitialized: true, cookie: { secure: false } }));
6.4 Add 'session_secret' in Config file:
session_secret = 'tsnodeprofiles_session';
- Check 'session' in index action
index(req, res, next) { if (!req.session.user) { return res.redirect('/login'); } res.render('home', { title: 'hello world' }); }
- Create a 'middlewares' folder.
- Add a 'authorizer.ts' file:
import * as express from 'express'; export class Authorizer { authorize(req, res, next) { if (!req.session.user) { return res.redirect('/login'); } next(); } }
- Use 'authorizer' middleware in Router:
import * as express from 'express'; import { HomeController } from './controllers/home.controller'; import { UsersController } from './controllers/users.controller'; import { Authorizer } from './middlewares/authorizer'; export class Router { router = express.Router(); homeController = new HomeController(); usersController = new UsersController(); authorizer = new Authorizer(); constructor() { this.router.get('/', this.authorizer.authorize, this.homeController.index); this.router.get('/users', this.authorizer.authorize, this.usersController.index); this.router.get('/users/create', this.authorizer.authorize, this.usersController.create); this.router.post('/users/update', this.authorizer.authorize, this.usersController.update); this.router.get('/users/edit', this.authorizer.authorize, this.usersController.edit); this.router.get('/users/delete', this.authorizer.authorize, this.usersController.delete); this.router.get('/login', this.homeController.login); this.router.post('/login', this.homeController.loginPost); } }
Deploy to Heroku, https://www.heroku.com/
- Download and install https://toolbelt.heroku.com/
- Config port with $PORT value:
export class Config { db = 'mongodb://localhost/tsnodeprofiles'; session_secret = 'tsnodeprofiles_session'; port = process.env.PORT || 3000; }
- Use the configed port in 'index.ts':
app.listen(config.port, function () { console.log(`Example app listening on port ${config.port}!`); });
- Add a file named 'Procfile' in the root folder of the project with following content:
web: node index.js
- Create a new website and push the code to deploy:
keroku login heroku create tsnodeprofile --buildpack heroku/nodejs git init heroku git:remote -a tsnodeprofile git add . git status git commit -am 'init' git config --global push.default simple git push keroku master
https://tsnodeprofile.herokuapp.com/