Angular 2 with Socket.IO Beer Tasting Party Study Application (Part 3)

Datetime:2016-08-22 22:30:12          Topic: AngularJS           Share

Preparing our Angular 2 Environment

We’re going to start off by setting up the environment as suggested in the Angular 2 Typescript Quickstart guide. We’re going to diverge a little bit and not use the ‘lite-server’ so that we can use gulp to watch and reset our server connection as needed.

First we’ll start with the suggested npm package.json file with a few modifications. We’re removing the “start” and “lite” scripts because we won’t be using them. Instead we will be using gulp. We also add to our dependencies a few things we will definitely need: express, gulp, gulp-nodemon, gulp-typescript, jasmine-core, mysql, and socket.io-client

package.json

{
  "name": "beer-tasting-app",
  "version": "1.0.0",
  "description": "Open Source Beer Tasting App",
  "scripts": {
    "postinstall": "typings install",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "typings": "typings"
  },
  "license": "ISC",
  "dependencies": {
    "@angular/common": "2.0.0-rc.4",
    "@angular/compiler": "2.0.0-rc.4",
    "@angular/core": "2.0.0-rc.4",
    "@angular/forms": "0.2.0",
    "@angular/http": "2.0.0-rc.4",
    "@angular/platform-browser": "2.0.0-rc.4",
    "@angular/platform-browser-dynamic": "2.0.0-rc.4",
    "@angular/router": "3.0.0-beta.2",
    "@angular/router-deprecated": "2.0.0-rc.2",
    "@angular/upgrade": "2.0.0-rc.4",
    "angular2-autosize": "^1.0.0",
    "angular2-cookie": "^1.2.2",
    "angular2-in-memory-web-api": "0.0.14",
    "bootstrap": "^3.3.6",
    "core-js": "^2.4.0",
    "express": "^4.14.0",
    "gulp": "^3.9.1",
    "gulp-nodemon": "^2.1.0",
    "gulp-typescript": "^2.13.6",
    "jasmine-core": "^2.4.1",
    "mysql": "^2.11.1",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "socket.io-client": "^1.4.8",
    "systemjs": "0.19.27",
    "unit.js": "^2.0.0",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "typescript": "^1.8.10",
    "typings": "^1.0.4"
  }
}

Then we will define our typescript by using the Angular 2 template and making some minor modifications. Create a new directory in the root of the project: ‘src’, then add to another directory ‘app’. We’ll add the tsconfig.json there. This will contain all of our Angular 2 code. Note, the change we’ve made here is to specify the outDir as relative to our root /dist/app . This will keep our source and our transpiled code separate and clean.

src/app/tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": false,
    "removeComments": true,
    "noLib": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "pretty": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitUseStrict": false,
    "noFallthroughCasesInSwitch": true,
    "outDir": "../../dist/app"
  },
  "compileOnSave": false
}

In the root of the application directory we’ll put the typings.json file.

typings.json

{
  "globalDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160602141332",
    "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
    "node": "registry:dt/node#6.0.0+20160621231320"
  }
}

Lastly and most importantly we’ll do the systemjs.config.js . There are 2 important additions here, I’m importing socket.io-client as well as angular2-cookie . These additions are in the ‘map’ and ‘packages’ objects.

systemjs.config.js

/**
 * System configuration for Angular 2 samples
 * Adjust as necessary for your application needs.
 */
(function(global) {
  // map tells the System loader where to look for things
  var map = {
    'app':                        '/dist/app', // 'dist',
    '@angular':                   '/node_modules/@angular',
    'angular2-in-memory-web-api': '/node_modules/angular2-in-memory-web-api',
    'rxjs':                       '/node_modules/rxjs',
    'socket.io-client': '/node_modules/socket.io-client/socket.io.js',
    'angular2-cookie': '/node_modules/angular2-cookie'
  };
  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
    'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
    'socket.io-client': {'defaultExtension': 'js'},
    'angular2-cookie': { main: 'core.js',  defaultExtension: 'js' }
  };
  var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'forms',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
  ];
  // Individual files (~300 requests):
  function packIndex(pkgName) {
    packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' };
  }
  // Bundled (~40 requests):
  function packUmd(pkgName) {
    packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
  }
  // Most environments should use UMD; some (Karma) need the individual index files
  var setPackageConfig = System.packageWithIndex ? packIndex : packUmd;
  // Add package entries for angular packages
  ngPackageNames.forEach(setPackageConfig);
  var config = {
    map: map,
    packages: packages
  };
  System.config(config);
})(this);

With the basic directory structure in place, and our tooling configured we need to install the modules by running npm install . Important note: if the ‘typings’ folder is missing, you should run npm run typings install .

To recap our file structure should now look like this:

.
├── package.json
├── node_modules
│   └── ... (many things)
├── src
│   └── app
│       └── tsconfig.json
├── systemjs.config.js
├── typings
│   ├── globals
│   │   ├── ... (ts deps)
│   └── index.d.ts
└── typings.json

At this point the joke I saw posted recently on a developer forum hits home “2016 the year when setting up your tooling takes longer than building your app.” �� Sad, but true.

Gulp setup

We’ve got a local version of gulp loaded, so let’s make use of it. The goal is to have a development environment that will watch for changes in our src/app/* area and compile typescript to JavaScript as well as copy all of our assets (css, images, html template etc.) to the dist/app/* area where our express server will be looking for static assets. Create a gulpfile.js in the root application folder.

gulpfile.js

'use strict';
let gulp = require('gulp');
let ts = require('gulp-typescript');
let nodemon = require('gulp-nodemon');

gulp.task('copy_assets', () => {
	return gulp.src(['./src/**/*', '!./**/*.ts'])
		.pipe(gulp.dest('dist'));
});

gulp.task('compile_ts', () => {
	var tsProject = ts.createProject('src/app/tsconfig.json');
	return tsProject.src()
		.pipe(ts(tsProject))
		.pipe(gulp.dest('dist/app'));
});

gulp.task('start', () => {
	nodemon({
		script: 'index.js',
		ext: 'js',
		watch: [
			'src/app/main.js',
			'modules/*'
		] // we only watch 1 file because everything gets recompiled on save.
  })
});

gulp.task('watch', ['start'], () => {
	gulp.watch('src/app/**/*', ['copy_assets','compile_ts']);
});

There are 3 main tasks here; copy_assets handles copying the source assets from src/* to dist/* ignoring the typescript files, compile_ts watches for changes in the typescript files and compiles on save, start watches for changes in our nodejs files and restarts the server on save. We’re using WebSockets as our data transport method which means I want to keep my WebSocket traffic clean and forget about using the ever popular BrowserSync.

Finally getting something in the browser!

In src/app add the base HTML file index.html . This is where we bootstrap Angular 2.

<!DOCTYPE html>
<html lang="en">
<head>
  <base href="/">
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Beer Tasting App</title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1; user-scalable=no">
  <link rel="stylesheet" href="/css/main.css">
  <script src="/node_modules/core-js/client/shim.min.js"></script>
  <script src="/node_modules/zone.js/dist/zone.js"></script>
  <script src="/node_modules/reflect-metadata/Reflect.js"></script>
  <script src="/node_modules/systemjs/dist/system.src.js"></script>
  <script src="/system.config.js"></script>
  <script>
  System.import('app')
    .catch(function (e) {
      console.error(e);
    });
  </script>
</head>
<body>

  

  
   
 Loading...

  

</body>
</html>

Create a CSS folder and file for all of the global CSS src/app/css/main.css

/* Reset */
html,
body,
div {
  border: 0;
  margin: 0;
  padding: 0;
  text-align:center;
}

/* Box-sizing border-box */
* {
  box-sizing: border-box;
}

/* Set up a default font and some padding to provide breathing room */
body {
  font-family: Roboto, "Helvetica Neue", sans-serif;
  font-size: 22px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: black;
  color: white;
  padding: 5px 0 20px 0;
}

h2 {
  font-size: 32px;
  text-transform: uppercase;
  font-weight: 300;
  text-align: center;
  padding: 0 20px;
}

p {
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 20px;
  margin-bottom: 1em;
  margin-top: 1em;
  padding: 0 20px;
}

ul {
  margin: 10px 0 0 0;
  padding: 0 0 0 20px;
}

li {
  font-weight: 400;
  margin-top: 4px;
}

a {
  color:white;
}

label {
	display:block;
}

input {
  width: 90%;
  margin: 12px 0;
  height: auto;
  background-color: #cae6ff;
  border: 2px solid #03b2fb;
  font-size: 25px;
  line-height: 37px;
  padding: 5px 10px;
}

button {
  background-color: #106cc8;
  border-style: none;
  color: rgba(255, 255, 254, 0.87);
  cursor: pointer;
  display: inline-block;
  font-size: 14px;
  height: 40px;
  padding: 8px 18px;
  text-decoration: none;
}



.button {
  padding: 8px 10px;
  display: block;
  font-size: 26px;
  font-weight: 800;
  text-align: center;
  border: 4px solid rgba(255, 255, 255, 0.68);
  width: 90%;
  margin: 18px auto;
  color: white;
  text-decoration: none;
}

.button.primary {
  background-color: #03b2fb;
}

.button.danger {
  background-color: #fb5503;
}

.button.secondary {
  background-color: white;
  color: #828181;
  border: 4px solid rgba(185, 185, 185, 0.68)
}

button:hover {
  background-color: #28739e;
}

.container {
  max-width: 600px;
  margin: 0 auto;
  position: relative;
}

.container .button.fixed {
  position: absolute;
  bottom: 20px;
  width: 90%;
}

.form {
  margin-bottom: 40px;
}

.splash {
  text-align:center;
}

.splash img {
  max-width: 180px;
}



p {
  font-weight: 300;
  letter-spacing: 1px;
  text-align: center;
  font-size: 24px;
  margin-bottom: 30px;
  line-height: normal;
}

Create an application entry point src/app/main.ts this is where the angular app actually gets bootstrapped with dependencies.

src/app/main.ts

import { APP_BASE_HREF } from '@angular/common';
import { disableDeprecatedForms, provideForms } from '@angular/forms';
import { enableProdMode, provide } from '@angular/core';
import { bootstrap } from '@angular/platform-browser-dynamic';

import { APP_ROUTER_PROVIDERS } from './app.routes';
import { AppComponent } from './app.component';

if ('<%= ENV %>' === 'prod') { enableProdMode(); }

/**
 * Bootstraps the application and makes the ROUTER_PROVIDERS and the APP_BASE_HREF available to it.
 */
bootstrap(AppComponent, [
  disableDeprecatedForms(),
  provideForms(),
  APP_ROUTER_PROVIDERS,
  {
    provide: APP_BASE_HREF,
    useValue: '/'
  },
]);

If you’ve got a syntax highlighter for TypeScript it’s probably complaining right now that we don’t actually have an app.component or an app.route component to import. Let’s create them.

src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { Router, ROUTER_DIRECTIVES } from '@angular/router';
import { HTTP_PROVIDERS } from '@angular/http';
import { CookieService } from 'angular2-cookie/core';

/**
 * This class represents the main application component. Within the @Routes annotation is the configuration of the
 * applications routes, configuring the paths for the lazy loaded components (HomeComponent, AboutComponent).
 */
@Component({
  moduleId: module.id,
  selector: 'sd-app',
  viewProviders: [HTTP_PROVIDERS],
  templateUrl: 'app.component.html',
  directives: [ROUTER_DIRECTIVES],
  providers: [CookieService]
})
export class AppComponent implements OnInit {
	window: Window;
  constructor(
    private router: Router
  ) { }
  ngOnInit() {
    // do init tasks
  }

}

The base component HTML will be a simple outlet for our router because we won’t be wrapping toolbars or other global objects around the interface.

src/app/app.component.html

<router-outlet></router-outlet>

Then declare a place where we will import our componentized routes.

src/app/app.routes.ts

import { provideRouter, RouterConfig } from '@angular/router';

import { HomeRoutes } from './+home/home.routes';

const routes: RouterConfig = [
  ...HomeRoutes,
];

export const APP_ROUTER_PROVIDERS = [
  provideRouter(routes),
];

TypeScript is complaining about yet another unresolved dependency which is our home.routes module. Create a new folder src/app/+home and add the routes declaration script file.

src/app/+home/home.routes.ts

import { RouterConfig } from '@angular/router';

import { HomeComponent } from './home.component';

export const HomeRoutes: RouterConfig = [
  {
    path: '',
    component: HomeComponent
  }
];

Next create the home component in the same file create all of the pieces of the home component.

src/app/+home/home.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { REACTIVE_FORM_DIRECTIVES } from '@angular/forms';

@Component({
  moduleId: module.id,
  selector: 'sd-home',
  templateUrl: 'home.component.html',
  styleUrls: ['home.component.css'],
  directives: [REACTIVE_FORM_DIRECTIVES],
  providers: []
})
export class HomeComponent {

  constructor(
		private router: Router
	) {}

}

src/app/+home/home.component.html

<div class="container">
  <h1>Beer Tasting Party</h1>
  <div class="splash">
    <img src="./assets/svg/beer.svg" alt="Beer Splash">
  </div>
  <p>A fun, self-hosted, open-source blind beer tasting party app.
</div>

For sake of completeness, the beer.svg and other asset files can be found here: assets .

We’re ready for express to start serving these assets so create a basic express script in the base folder.

index.js

var express = require('express'),
	app = express(),
	server = require('http').createServer(app),
	io = require('socket.io').listen(server),
	path = require('path'),
	fs = require('fs');

app.use(express.static(path.join(__dirname, '/')));
app.use(express.static(path.join(__dirname, '/dist/app/')));

app.get('*', function (req, res) {
	res.set('Content-Type', 'text/html');
  res.send(fs.readFileSync(__dirname+'/dist/app/index.html'));
});

server.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

One small addition. We need to tell TypeScript where to look for definitions. In src/app create a file typings.d.ts and add the contents /// <reference path="../../typings/index.d.ts"/>

Now run npm install socket.io --save ; we’ll be using this in the next section. Once that is down, we should be able to run gulp watch and build our application!

Tims-MacBook-Pro:bee.rs tim$ gulp watch
[15:19:53] Using gulpfile ~/Documents/www/bee.blog/gulpfile.js
[15:19:53] Starting 'start'...
[15:19:53] Finished 'start' after 1.58 ms
[15:19:53] Starting 'watch'...
[15:19:53] Finished 'watch' after 13 ms
[15:19:53] [nodemon] 1.9.2
[15:19:53] [nodemon] to restart at any time, enter `rs`
[15:19:53] [nodemon] watching: src/app/main.js modules/**/*
[15:19:53] [nodemon] starting `node index.js`
Example app listening on port 3000!

Now open a TypeScript file and save it. We should see gulp doing its thing and compiling the application as well as moving over the assets. If you open http://127.0.0.1:3000 you should now see the splash page!

We now have a working implementation of Angular 2! To recap this should be our file structure.

.
├── dist
│   └── (we don't edit this)
├── gulpfile.js
├── index.js
├── package.json
├── src
│   └── app
│       ├── +home
│       │   ├── home.component.css
│       │   ├── home.component.html
│       │   ├── home.component.ts
│       │   └── home.routes.ts
│       ├── app.component.html
│       ├── app.component.ts
│       ├── app.routes.ts
│       ├── assets
│       │   └── svg
│       │       └── beer.svg
│       ├── css
│       │   └── main.css
│       ├── index.html
│       ├── main.ts
│       ├── tsconfig.json
│       └── typings.d.ts
├── systemjs.config.js
├── typings
│   ├── globals
│   │   ├── ( etc ... )
│   └── index.d.ts
└── typings.json

InPart 4 we will:

  • Build out some Angular 2 components.
  • Build out some Angular 2 services.
  • Setup a testing environment.




About List