<!DOCTYPE html>
<html>
<head>
<base href="./" />
<title>Angular 2/4 - Animation Tutorial & Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap css -->
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<!-- application less -->
<link href="app/_content/app.less" rel="stylesheet/less" type="text/css" />
<!-- compiling less on client side for the example only, this should be done on the server in a production app -->
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/2.5.3/less.min.js"></script>
<!-- polyfill(s) for older browsers -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://unpkg.com/zone.js@0.6.23?main=browser"></script>
<script src="https://unpkg.com/reflect-metadata@0.1.3"></script>
<script src="https://unpkg.com/systemjs@0.19.27/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) { console.error(err); });
</script>
</head>
<body>
<app>Loading...</app>
</body>
</html>
/**
* WEB ANGULAR VERSION
* (based on systemjs.config.js in angular.io)
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
transpiler: 'ts',
typescriptOptions: {
// Complete copy of compiler options in standard tsconfig.json
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"sourceMap": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5"
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
// paths serve as alias
'npm:': 'https://unpkg.com/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core@4.0.2/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common@4.0.2/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler@4.0.2/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser@4.0.2/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@4.0.2/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http@4.0.2/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router@4.0.2/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms@4.0.2/bundles/forms.umd.js',
'@angular/animations': 'npm:@angular/animations@4.0.2/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations@4.0.2/bundles/animations-browser.umd.js',
'@angular/platform-browser/animations': 'npm:@angular/platform-browser@4.0.2/bundles/platform-browser-animations.umd.js',
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.2.1/lib/typescript.js',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
}
}
});
if (!global.noBootstrap) { bootstrap(); }
// Bootstrap the `AppModule`(skip the `app/main.ts` that normally does this)
function bootstrap() {
// Stub out `app/main.ts` so System.import('app') doesn't fail if called in the index.html
System.set(System.normalizeSync('app/main.ts'), System.newModule({ }));
// bootstrap and launch the app (equivalent to standard main.ts)
Promise.all([
System.import('@angular/platform-browser-dynamic'),
System.import('app/app.module')
])
.then(function (imports) {
var platform = imports[0];
var app = imports[1];
platform.platformBrowserDynamic().bootstrapModule(app.AppModule);
})
.catch(function(err){ console.error(err); });
}
})(this);
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
import { Component } from '@angular/core';
import { ProductService } from './_services/index';
@Component({
moduleId: module.id.toString(),
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent {
constructor(private productService: ProductService) {
// add some initial products
if (productService.getAll().length === 0) {
productService.save({ name: 'Boardies', price: '25.00' });
productService.save({ name: 'Singlet', price: '9.50' });
productService.save({ name: 'Thongs (Flip Flops)', price: '12.95' });
}
}
}
<!-- header -->
<header>
<ul class="nav nav-tabs">
<li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"><a routerLink="/">Home</a></li>
<li routerLinkActive="active"><a routerLink="/products">Products</a></li>
</ul>
</header>
<main>
<router-outlet></router-outlet>
</main>
<!-- footer -->
<footer>
<p>
<a href="http://jasonwatmore.com/post/2017/04/19/angular-2-4-router-animation-tutorial-example" target="_top">Angular 2/4 - Router Animation Tutorial & Example</a>
</p>
<p>
<a href="http://jasonwatmore.com" target="_top">JasonWatmore.com</a>
</p>
</footer>
export * from './product.service';
export * from './pub-sub.service';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { AppRoutingModule, routedComponents } from './app-routing.module';
import { ProductService, PubSubService } from './_services/index';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
BrowserAnimationsModule
],
declarations: [
AppComponent,
routedComponents
],
providers: [
ProductService,
PubSubService
],
bootstrap: [AppComponent]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/index';
import { ProductListComponent, ProductAddEditComponent } from './products/index';
const routes: Routes = [
{ path: '', pathMatch: 'full', component: HomeComponent },
{
path: 'products',
component: ProductListComponent,
children: [
{ path: 'add', component: ProductAddEditComponent },
{ path: 'edit/:id', component: ProductAddEditComponent }
]
},
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
export const routedComponents = [HomeComponent, ProductListComponent, ProductAddEditComponent];
import { Component } from '@angular/core';
import { fadeInAnimation } from '../_animations/index';
@Component({
moduleId: module.id.toString(),
templateUrl: 'home.component.html',
animations: [fadeInAnimation],
host: { '[@fadeInAnimation]': '' }
})
export class HomeComponent {
}
<h1>Home</h1>
<p>Angular 2/4 - Animation Tutorial & Example</p>
export * from './home.component';
/* GENERAL STYLES AND CLASSES
--------------------------------------------------------------------- */
body {
padding: 5px;
ng-component {
display: block;
}
}
/* HEADER STYLES
--------------------------------------------------------------------- */
header {
}
/* MAIN STYLES
--------------------------------------------------------------------- */
main {
padding: 0 20px;
min-height: 300px;
.products-table {
margin-top: 20px;
.delete-column {
width: 60px;
}
}
/* side form */
.view-side-form {
.side-form {
position: absolute;
z-index: 100;
top: 0;
right: 0;
width: 80%;
height: 100%;
overflow: auto;
background: #fff;
padding: 20px;
border-left: 1px solid #e0e0e0;
}
}
}
/* FOOTER STYLES
--------------------------------------------------------------------- */
footer {
text-align: center;
margin-top: 20px;
padding: 20px;
border-top: 1px solid #ddd;
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { fadeInAnimation } from '../_animations/index';
import { ProductService, PubSubService } from '../_services/index';
@Component({
moduleId: module.id.toString(),
templateUrl: 'product-list.component.html',
animations: [fadeInAnimation],
host: { '[@fadeInAnimation]': '' }
})
export class ProductListComponent implements OnInit, OnDestroy {
products: any[];
subscription: Subscription;
constructor(
private productService: ProductService,
private pubSubService: PubSubService) { }
deleteProduct(id: number) {
this.productService.delete(id);
this.loadProducts();
}
ngOnInit() {
this.loadProducts();
// reload products when updated
this.subscription = this.pubSubService.on('products-updated').subscribe(() => this.loadProducts());
}
ngOnDestroy() {
// unsubscribe to ensure no memory leaks
this.subscription.unsubscribe();
}
private loadProducts() {
this.products = this.productService.getAll();
}
}
<h1>Products</h1>
<a routerLink="add" class="btn btn-default">Add Product</a>
<table class="table products-table">
<tr>
<th>Name</th>
<th>Price</th>
<th class="delete-column"></th>
</tr>
<tr *ngFor="let product of products">
<td><a [routerLink]="['edit', product.id]">{{product.name}}</a></td>
<td>${{product.price}}</td>
<td><a (click)="deleteProduct(product.id)" class="btn btn-xs btn-danger">Delete</a></td>
</tr>
</table>
<div class="view-side-form">
<router-outlet></router-outlet>
</div>
export * from './product-list.component';
export * from './product-add-edit.component';
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { slideInOutAnimation } from '../_animations/index';
import { ProductService, PubSubService } from '../_services/index';
@Component({
moduleId: module.id.toString(),
templateUrl: 'product-add-edit.component.html',
animations: [slideInOutAnimation],
host: { '[@slideInOutAnimation]': '' }
})
export class ProductAddEditComponent implements OnInit {
title = "Add Product";
product: any = {};
constructor(
private route: ActivatedRoute,
private router: Router,
private productService: ProductService,
private pubSubService: PubSubService) { }
ngOnInit() {
let productId = Number(this.route.snapshot.params['id']);
if (productId) {
this.title = 'Edit Product';
this.product = this.productService.getById(productId);
}
}
saveProduct() {
// save product
this.productService.save(this.product);
// redirect to users view
this.router.navigate(['products']);
// publish event so list controller can refresh
this.pubSubService.publish('products-updated');
}
}
<div class="side-form">
<h1>{{title}}</h1>
<div class="form-container">
<form (ngSubmit)="saveProduct()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" name="name" [(ngModel)]="product.name" class="form-control" required />
</div>
<div class="form-group">
<label for="name">Price</label>
<input type="text" name="price" [(ngModel)]="product.price" class="form-control" required />
</div>
<div class="form-group">
<a class="btn btn-default" routerLink="/products">Cancel</a>
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';
@Injectable()
export class PubSubService {
private subjects: Subject<any>[] = [];
publish(eventName: string) {
// ensure a subject for the event name exists
this.subjects[eventName] = this.subjects[eventName] || new Subject<any>();
// publish event
this.subjects[eventName].next();
}
on(eventName: string): Observable<any> {
// ensure a subject for the event name exists
this.subjects[eventName] = this.subjects[eventName] || new Subject<any>();
// return observable
return this.subjects[eventName].asObservable();
}
}
import { Injectable } from '@angular/core';
@Injectable()
export class ProductService {
getAll() {
return this.getProducts();
}
getById(id: number) {
return this.getProducts().find(product => product.id === id);
}
save(product: any) {
let products = this.getProducts();
if (product.id) {
// update existing product
for (var i = 0; i < products.length; i++) {
if (products[i].id === product.id) {
products[i] = product;
break;
}
}
this.setProducts(products);
} else {
// create new product
// assign id
var lastProduct = products[products.length - 1] || { id: 0 };
product.id = lastProduct.id + 1;
// save to local storage
products.push(product);
this.setProducts(products);
}
}
delete(id: number) {
let products = this.getProducts();
for (var i = 0; i < products.length; i++) {
var product = products[i];
if (product.id === id) {
products.splice(i, 1);
break;
}
}
this.setProducts(products);
}
// private helper methods
private getProducts(): any[] {
if (!localStorage.getItem('products')) {
localStorage.setItem('products', JSON.stringify([]));
}
return JSON.parse(localStorage.getItem('products'));
}
private setProducts(products: any[]) {
localStorage.setItem('products', JSON.stringify(products));
}
}
import { trigger, state, animate, transition, style } from '@angular/animations';
export const fadeInAnimation =
trigger('fadeInAnimation', [
// route 'enter' transition
transition(':enter', [
// styles at start of transition
style({ opacity: 0 }),
// animation and styles at end of transition
animate('.3s', style({ opacity: 1 }))
]),
]);
import { trigger, state, animate, transition, style } from '@angular/animations';
export const slideInOutAnimation =
trigger('slideInOutAnimation', [
// end state styles for route container (host)
state('*', style({
// the view covers the whole screen with a semi tranparent background
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)'
})),
// route 'enter' transition
transition(':enter', [
// styles at start of transition
style({
// start with the content positioned off the right of the screen,
// -400% is required instead of -100% because the negative position adds to the width of the element
right: '-400%',
// start with background opacity set to 0 (invisible)
backgroundColor: 'rgba(0, 0, 0, 0)'
}),
// animation and styles at end of transition
animate('.5s ease-in-out', style({
// transition the right position to 0 which slides the content into view
right: 0,
// transition the background opacity to 0.8 to fade it in
backgroundColor: 'rgba(0, 0, 0, 0.8)'
}))
]),
// route 'leave' transition
transition(':leave', [
// animation and styles at end of transition
animate('.5s ease-in-out', style({
// transition the right position to -400% which slides the content out of view
right: '-400%',
// transition the background opacity to 0 to fade it out
backgroundColor: 'rgba(0, 0, 0, 0)'
}))
])
]);
export * from './fade-in.animation';
export * from './slide-in-out.animation';