#MEAN Session 12
##Angular 2 Sample
- uses import (modules) instead of
<script ... > - uses typescript (.ts) files
- uses Classes
- no angular.module() in the traditional sense
Note: "@" indicates a scoped package.
e.g. import { NgModule } from '@angular/core';
{ ... } curly brackets indicate a single method from a module
##ES6 Modules
Not yet supported by all browsers. One solution that is common:
- Webpack for modules
- Babel for ES6
###Babel es6 Conversion
https://babeljs.io
Try it out:
const box = document.querySelector('.box');
box.addEventListener('click', function() {
let first = 'opening';
let second = 'open';
if(this.classList.contains(first)){
[first, second] = [second, first];
}
this.classList.toggle(first);
setTimeout(() => {
this.classList.toggle(second);
}, 500);
});##Webpack
$ mkdir es6modules
$ cd es6modules
$ touch app.js
- getting modules: npm install with the following payload:
{
"name": "es6modules",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
},
"author": "",
"license": "ISC",
"dependencies": {
"flickity": "^1.2.1",
"insane": "^2.5.0",
"jquery": "^3.0.0",
"jsonp": "^0.2.0",
"lodash": "^4.13.1"
},
"devDependencies": {
}
}index.html
<!DOCTYPE html>
<html>
<head>
<title>Recipes</title>
</head>
<body>
</body>
<script src="app.js"></script>
</html>In app.js:
import { uniq } from 'lodash';
import insane from 'insane';
import jsonp from 'jsonp';- Error: import not supported
###Babel
- First Install your dependencies:
$ npm install webpack@beta babel-loader babel-core babel-preset-es2015-native-modules --save-dev- Create a
webpack.config.jsfile:
const webpack = require('webpack');
const nodeEnv = process.env.NODE_ENV || 'production';
module.exports = {
devtool: 'source-map',
entry: {
filename: './app.js'
},
output: {
filename: '_build/bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015-native-modules']
}
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
comments: false
},
sourceMap: true
}),
new webpack.DefinePlugin({
'process.env': { NODE_ENV: JSON.stringify(nodeEnv) }
})
]
};- Setup the build npm script in
package.json:
"scripts": {
"build": "webpack --progress --watch"
},$ npm run build
-
change script src in index.html to point to
_build/bundle.js -
use lodash in app.js
import { uniq } from 'lodash';
import insane from 'insane';
import jsonp from 'jsonp';
const ages = [2, 34, 6, 1, 86, 34];
console.log(uniq(ages));Note that Webpack runs on save and uses js mapping in the browser console
###Roll Your Own Module
$ mkdir mymodule
$ touch config.js
const apikey = '123abc';
// default
export default apikey;default and named exports (for methods and variables)
import { uniq } from 'lodash';
import insane from 'insane';
import jsonp from 'jsonp';
import apikey from './mymodule/config';console.log(apikey)
Default exports can be renamed on import.
Named export:
// named
export const apikey = '123abc';
// default
// export default apikey;Check terminal for webpack message.
Named exports cannot be renamed on import.
We need to import it as follows:
import { apikey } from './mymodule/config';
e.g.
import { uniq } from 'lodash';
import insane from 'insane';
import jsonp from 'jsonp';
import { apikey } from './mymodule/config'##Classes
-
not new to JS - just a new syntax
-
prototypal inheritance review
classes.html
In browser console:
> const names = ['hamburger', 'fries']
> names.join()
> names.pop()
> Array.prototype and examine the prototype
> lasagna.eat()
> lasagna and examine the prototype
Declaration and mandatory constructor method:
class Recipe {
constructor(name, country){
this.name = name;
this.country = country;
}
}
const lasagna = new Recipe('Lasagna', 'Italy');
const pho = new Recipe('Pho', 'Vietnam');> lasagna
Creating methods inside a class (no comma)
class Recipe {
constructor(name, country){
this.name = name;
this.country = country;
}
cook(){
console.log(`I like to cook ${this.name}`);
}
eat(){
console.log(`Yummy!`);
}
}
const lasagna = new Recipe('Lasagna', 'Italy');
const pho = new Recipe('Pho', 'Vietnam');> lasagna.cook returns the function
> lasagna.cook() to run the function
Getters, setters, and statics methods
class Recipe {
constructor(name, country){
this.name = name;
this.country = country;
}
cook(){
console.log(`I like to cook ${this.name}`);
}
eat(){
console.log(`Yummy!`);
}
get description() {
return `${this.name} is from ${this.country}.`;
}
get description() {
return `${this.name} is from ${this.country}.`;
}
set ingredients(value) {
this.ingredient = value.trim();
}
get ingredients() {
return this.ingredient.toUpperCase();
}
}
const lasagna = new Recipe('Lasagna', 'Italy');
const pho = new Recipe('Pho', 'Vietnam');> lasagna.description
> lasagna.ingredients = ' pasta '
> lasagna.ingredients
##TypeScript
A superset of the JavaScript programming language that adds the concept of static typing to the core features of JavaScript.
###Babel vs TypeScript
Playgrounds:
http://www.typescriptlang.org/play/index.html
Note the example using classes.
https://babeljs.io/repl/
Run this script in both Babel and TypeScript
const box = document.querySelector('.box');
box.addEventListener('click', function() {
let first = 'opening';
let second = 'open';
if(this.classList.contains(first)){
[first, second] = [second, first];
}
this.classList.toggle(first);
setTimeout(() => {
this.classList.toggle(second);
}, 500);
});Note: Type errors do not prevent JavaScript emit.
To see variable type checking in action:
var foo = 123;
foo = '456';or
function speak(value: string){
console.log(value);
}
var greeted = 'world';
var greeting = 'hello';
var whatToSay = greeting + greeted;
speak(whatToSay)
Typescript will catch errors before you run them. Babel will not.
###Running Typescript
npm install -g typescript@next
tsc to verify
Make directory typescript and open in VSCode
index.html:
<!doctype html>
<head>
<title>TypeScript</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
<div id="container" class="container">
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>Create app.ts
$ tsc ./app.ts or ``tsc -w ./app.ts` then
let myString: string = "Hello world"
string = 12;$ tsc to see the configuration options.
tsconfig.json
{
"compilerOptions": {
"target": "es5"
}
}
####Template Strings
var container = document.getElementById('container');
var todo = {
id: 123,
name: 'Pick up drycleaning',
completed: true
}
container.innerHTML = `
<div todo='${todo.id}' class="list-group-item}">
<i class="${ todo.completed ? "" : "hidden" } text-success glyphicon glyphicon-ok"></i>
<span class="name">${todo.name}</span>
</div>
`####For in loops
var array = [
"Pick up drycleaning",
"Clean Batcave",
"Save Gotham"
];
// try for in first
// need to grab the value via the index
// for (var index in array) {
// var value = array[index];
// console.log(`${index}: ${value}`);
// }
for (var value of array) {
console.log(`${value}`);
}####this rebinding with fat arrows
old:
var container = document.getElementById('container');
function Counter(el) {
this.count = 0;
el.innerHTML = this.count;
el.addEventListener('click',
function(){
this.count += 1;
el.innerHTML = this.count;
})
}
new Counter(container);NaN
var container = document.getElementById('container');
function Counter(el) {
this.count = 0;
el.innerHTML = this.count;
let _this = this;
el.addEventListener('click',
function() {
_this.count += 1;
el.innerHTML = this.count;
})
}
new Counter(container);var container = document.getElementById('container');
function Counter(el) {
this.count = 0;
el.innerHTML = this.count;
el.addEventListener('click',
() => {
this.count += 1;
el.innerHTML = this.count;
})
}
new Counter(container);####Destructuring
var a = 1;
var b = 5;
[a,b] = [b,a];
##Angular 2
$ npm install
$ npm start
app.component.ts (AppComponent class)
export class AppComponent {
title = 'Recipe List';
recipe = 'Lasagna';
}@Component
template: `<h1>{{title}}</h1><h2>{{recipe}} details</h2>`app.component.ts (Recipe class)
- convert recipe from a literal string to a class:
import { Component } from '@angular/core';
export class Recipe {
id: number;
name: string;
}Now that we have a class let's refactor the component's recipe property to be of type Recipe and initialize it with values:
export class AppComponent {
title = 'Recipe List';
recipe: Recipe = {
id: 1,
name: 'Lasagna'
};
}Our template needs updated bindings to refer to the recipe's name property:
template: `
<h1>{{title}}</h1>
<h2>{{recipe.name}} details!</h2>`Expand the HTML:
template: `
<h1>{{title}}</h1>
<h2>{{recipe.name}} details.</h2>
<div><label>id: </label>{{recipe.id}}</div>
<div>
<label>name: </label>{{recipe.name}}
<input value="{{recipe.name}}" placeholder="name">
</div>`###Binding
There is no more $scope!
Before we can use two-way data binding for form inputs, we need to import the FormsModule package in our Angular module. We add it to the NgModule decorator's imports array. This array contains the list of external modules used by our application. Now we have included the forms package which includes ngModel.
app.module.ts (FormsModule import)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }Replace the <input> with the following HTML
<input [(ngModel)]="recipe.name" placeholder="name">###Master Detail
Add data to our app using a typed constant:
const RECIPES: Recipe[] = [
{ id: 1309, name: 'Lasagna' },
{ id: 1310, name: 'Pho Noodle Soup' },
{ id: 1311, name: 'Hamburger' },
{ id: 1312, name: 'Guacamole' }
];Create a public property in AppComponent that exposes the heroes for binding
app.component.ts (recipe array property)
recipes = RECIPES;e.g.
export class AppComponent {
title = 'Recipe List';
recipes = RECIPES;We do not have to define the recipes type. TypeScript can infer it from the RECIPES array.
Displaying recipes in a template.
Insert the following chunk of HTML below the title and above the recipe details.
<h2>Recipe List</h2>
<ul class="recipes">
<li>
<!-- each recipe goes here -->
</li>
</ul>First modify the <li> tag by adding the built-in directive *ngFor
<li *ngFor="let recipe of recipes">The (*) prefix to ngFor indicates that the <li> element and its children constitute a master template.
The ngFor directive iterates over the recipes array returned by the AppComponent.recipes property and stamps out instances of this template.
The quoted text assigned to ngFor means “take each recipe in the recipes array, store it in the local recipe variable, and make it available to the corresponding template instance”.
The let keyword before "recipe" identifies recipe as a template input variable. We can reference this variable within the template to access a recipe's properties.
<li *ngFor="let recipe of recipes">
<span class="badge">{{recipe.id}}</span> {{recipe.name}}
</li>Modify the <li> by inserting an Angular event binding to its click event.
<li *ngFor="let recipe of recipes" (click)="onSelect(recipe)">selectedRecipe: Recipe;
export class AppComponent {
title = 'Recipe List';
recipes = RECIPES;
selectedRecipe: Recipe;
onSelect(recipe: Recipe): void {
this.selectedRecipe = recipe;
}
}<div>
<label>name: </label>
<input [(ngModel)]="selectedRecipe.name" placeholder="name"/>
</div>
When our app loads we see a list of recipes, but a recipe is not selected. The selectedRecipe is undefined. That’s why we'll see an error in the browser’s console.
Wrap the HTML recipe detail content of our template with a <div>. Then add the ngIf built-in directive and set it to the selectedRecipe property of our component.
<div *ngIf="selectedRecipe">
<h2>...
</div>
Be sure to refer to the new selectedRecipe prop:
<div *ngIf="selectedRecipe">
<h2>{{selectedRecipe.name}} details.</h2>
<div><label>id: </label>{{selectedRecipe.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedRecipe.name" placeholder="name"/>
</div>
</div>
Manipulating classes in Angular 2:
<ul class="recipes">
<li *ngFor="let recipe of recipes" [class.selected]="recipe === selectedRecipe" (click)="onSelect(recipe)">
<span class="badge">{{recipe.id}}</span> {{recipe.name}}
</li>
</ul>
Add CSS to our @Component:
styles: [`
.selected {
background-color: #333 !important;
color: white;
}
`]Square brackets ([]) are the syntax for a property binding in which data flows one way from the data source (the expression recipe === selectedRecipe) to a property of class.
###Detail Component
Add a new file app/recipe-detail.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'my-recipe-detail',
})
export class RecipeDetailComponent {
}
Begin by importing the Component and Input decorators from Angular because we're going to need them.
Create metadata with the @Component decorator where we specify the selector name that identifies this component's element. Then we export the class to make it available to other components.
When we finish, we'll import it into AppComponent and create a corresponding element.
Replace selectedRecipe with recipe in the template:
<div *ngIf="recipe">
<h2>{{recipe.name}} details</h2>
<div><label>id: </label>{{recipe.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="recipe.name" placeholder="recipe name" />
</div>
</div>
The recipe property - recipe: Recipe; - is needed in both components so we make a new file.
/common/recipe.ts
export class Recipe {
id: number;
name: string;
}then in both components:
import { Recipe } from './common/recipe';Declare recipe as an input in recipe-detail.component:
export class RecipeDetailComponent {
@Input()
recipe: Recipe;
}Final recipe-detail.component:
import { Component, Input } from '@angular/core';
import { Recipe } from '../common/recipe';
@Component({
selector: 'my-recipe-detail',
templateUrl: './app/recipe-detail/recipe-detail.html'
})
export class RecipeDetailComponent {
@Input()
recipe: Recipe;
}Now the app.module
import { RecipeDetailComponent } from './recipe-detail.component';
...
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [AppComponent, RecipeDetailComponent],
bootstrap: [AppComponent]
})bind the selectedRecipe property of app.component to the recipe in recipe-detail.component :
<my-recipe-detail [recipe]="selectedRecipe"></my-recipe-detail>
Final app.component:
import { Component } from '@angular/core';
import { Recipe } from './common/recipe';
const RECIPES: Recipe[] = [
{ id: 1309, name: 'Lasagna' },
{ id: 1310, name: 'Pho Noodle Soup' },
{ id: 1311, name: 'Hamburger' },
{ id: 1312, name: 'Guacamole' }
];
@Component({
selector: 'my-app',
templateUrl: './app/app.component.html'
})
export class AppComponent {
title = 'Recipe List';
recipes = RECIPES;
selectedRecipe: Recipe;
onSelect(recipe: Recipe): void {
this.selectedRecipe = recipe;
}
}