<!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';