<!DOCTYPE html>
<html>

<head>
    <link href="style.css" type="text/css" rel="stylesheet"/>
    <base href="."/>
    <title>Templating Components Demo</title>
    <link rel="stylesheet" href="style.css"/>
    
    <script src="https://unpkg.com/core-js/client/shim.min.js"></script>
    <script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
    <script src="https://unpkg.com/reflect-metadata@0.1.8"></script>
    <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
    
    <script src="config.js"></script>
    <script>
        System.import('app')
            .catch(console.error.bind(console));
    </script>
</head>

  <body>
    <my-app>
    loading...
  </my-app>
  </body>

</html>
body {
    font-family: 'Segoe UI', sans-serif;
}

span {
    font-style: italic;
}

.item-container {
    margin: 8px;
    padding: 8px;
    width: 450px;
    border: 1px solid darkslategrey;
    border-radius: 5px;
}

.item-header {
    font-size: 1.3rem;
}

.item-container-selected .item-header {
    font-weight: bold;    
}


.item-details {
    color: dimgrey;
    font-size: 0.9rem;
}

.item-details-gender-male {
    color: blue;
}

.item-details-gender-female {
    color:deeppink;
}
### Templating Components Demo

It is source code for a corresponding article (still in progress)
System.config({
  //use typescript for compilation
  transpiler: 'typescript',
  //typescript compiler options
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  paths: {
    'npm:': 'https://unpkg.com/'
  },
  //map tells the System loader where to look for things
  map: {
    
    'app': './src',
    
    '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
    '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
    '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
    '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
    '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
    '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
    '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
    '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
    
    '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
    '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
    '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
    '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
    '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
    '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
    '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
    
    'rxjs': 'npm:rxjs',
    'typescript': 'npm:typescript@2.0.2/lib/typescript.js'
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    },
    rxjs: {
      defaultExtension: 'js'
    }
  }
});
//main entry point
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app';

platformBrowserDynamic().bootstrapModule(AppModule)
import {Component, NgModule} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { ListNavigatorModule } from "./controls/list-navigator/list-navigator.module";
import { Widget } from "./controls/widget/widget.component";

type User = {
    id: number;
    firstName: string;
    lastName: string;
    gender: string;
    email: string;
 }

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>widget demo</h2>
      <widget>
        <div class="content">
          <button>
            Just do my job...
          </button>
        </div>
        <div class="settings">
          <select>
            <option selected="true">Faster</option>
            <option>Slower</option>
          </select>
        </div>
      </widget>
      
      <h2>list navigator demo</h2>
      <list-navigator [dataSource]="dataSource" [(selectedItems)]="selectedUsers">
        <div 
            *list-navigator-item="let i, let isSelected = selected" 
            class="item-container" 
            [class.item-container-selected]="isSelected"
        >
            <div class="item-header">{{i.firstName}} {{i.lastName}}</div>
            <div class="item-details">
                Id: {{i.id}}, Email: {{i.email}}, Gender: <span [ngClass]="'item-details-gender-'+ i.gender.toLowerCase()">{{i.gender}}</span>
            </div>
        </div>
      </list-navigator>
    </div>
    <div>
        Selected: {{getSelectedUsersText()}}.
    <div>
`
})
export class App {

    protected dataSource: (offset: number, pageSize: number) => User[];

    protected selectedUsers: User[];

    constructor() {
        this.selectedUsers = this._data.slice(1, 2);
        this.dataSource = (o, p) => this._data.slice(o, o + p);
    }

    protected getSelectedUsersText(): string
    {
        if (this.selectedUsers.length < 1) {
            return "none";
        }
        return this.selectedUsers.map(i => `${i.firstName} ${i.lastName}`).join(", ");
    }

    private readonly _data: User[] = [{ "id": 1, "firstName": "Stephanie", "lastName": "Owens", "email": "sowens0@cmu.edu", "gender": "Female" }, { "id": 2, "firstName": "Stephen", "lastName": "Torres", "email": "storres1@cbc.ca", "gender": "Male" }, { "id": 3, "firstName": "Lillian", "lastName": "West", "email": "lwest2@jalbum.net", "gender": "Female" }, { "id": 4, "firstName": "Larry", "lastName": "Hill", "email": "lhill3@phpbb.com", "gender": "Male" }, { "id": 5, "firstName": "Patrick", "lastName": "Duncan", "email": "pduncan4@prlog.org", "gender": "Male" }, { "id": 6, "firstName": "Steven", "lastName": "Nguyen", "email": "snguyen5@meetup.com", "gender": "Male" }, { "id": 7, "firstName": "Eugene", "lastName": "Garrett", "email": "egarrett6@psu.edu", "gender": "Male" }, { "id": 8, "firstName": "Juan", "lastName": "Wood", "email": "jwood7@elpais.com", "gender": "Male" }, { "id": 9, "firstName": "Harold", "lastName": "Little", "email": "hlittle8@walmart.com", "gender": "Male" }, { "id": 10, "firstName": "Linda", "lastName": "Cunningham", "email": "lcunningham9@storify.com", "gender": "Female" }, { "id": 11, "firstName": "Mary", "lastName": "Clark", "email": "mclarka@jalbum.net", "gender": "Female" }, { "id": 12, "firstName": "Larry", "lastName": "Barnes", "email": "lbarnesb@usda.gov", "gender": "Male" }, { "id": 13, "firstName": "Kathy", "lastName": "Kelley", "email": "kkelleyc@delicious.com", "gender": "Female" }, { "id": 14, "firstName": "Johnny", "lastName": "Morris", "email": "jmorrisd@github.com", "gender": "Male" }, { "id": 15, "firstName": "Louis", "lastName": "Sanchez", "email": "lsancheze@usda.gov", "gender": "Male" }, { "id": 16, "firstName": "Arthur", "lastName": "Carter", "email": "acarterf@merriam-webster.com", "gender": "Male" }, { "id": 17, "firstName": "Mildred", "lastName": "Cole", "email": "mcoleg@deviantart.com", "gender": "Female" }, { "id": 18, "firstName": "Philip", "lastName": "Harvey", "email": "pharveyh@arstechnica.com", "gender": "Male" }, { "id": 19, "firstName": "Christine", "lastName": "Morales", "email": "cmoralesi@hibu.com", "gender": "Female" }, { "id": 20, "firstName": "Martha", "lastName": "Ford", "email": "mfordj@forbes.com", "gender": "Female" }, { "id": 21, "firstName": "Alice", "lastName": "Mccoy", "email": "amccoyk@google.cn", "gender": "Female" }, { "id": 22, "firstName": "Raymond", "lastName": "Chapman", "email": "rchapmanl@indiegogo.com", "gender": "Male" }, { "id": 23, "firstName": "Kathleen", "lastName": "Butler", "email": "kbutlerm@canalblog.com", "gender": "Female" }, { "id": 24, "firstName": "Diane", "lastName": "Baker", "email": "dbakern@unc.edu", "gender": "Female" }, { "id": 25, "firstName": "Ruth", "lastName": "Hill", "email": "rhillo@icq.com", "gender": "Female" }, { "id": 26, "firstName": "Margaret", "lastName": "Johnson", "email": "mjohnsonp@guardian.co.uk", "gender": "Female" }, { "id": 27, "firstName": "Virginia", "lastName": "Carpenter", "email": "vcarpenterq@altervista.org", "gender": "Female" }, { "id": 28, "firstName": "Lillian", "lastName": "Mitchell", "email": "lmitchellr@youtu.be", "gender": "Female" }, { "id": 29, "firstName": "Julie", "lastName": "Patterson", "email": "jpattersons@example.com", "gender": "Female" }, { "id": 30, "firstName": "Joyce", "lastName": "Garcia", "email": "jgarciat@who.int", "gender": "Female" }, { "id": 31, "firstName": "Charles", "lastName": "Gray", "email": "cgrayu@smugmug.com", "gender": "Male" }, { "id": 32, "firstName": "Anthony", "lastName": "Carr", "email": "acarrv@nih.gov", "gender": "Male" }, { "id": 33, "firstName": "Antonio", "lastName": "Hernandez", "email": "ahernandezw@fda.gov", "gender": "Male" }, { "id": 34, "firstName": "Nancy", "lastName": "Campbell", "email": "ncampbellx@canalblog.com", "gender": "Female" }, { "id": 35, "firstName": "Amanda", "lastName": "Wood", "email": "awoody@phoca.cz", "gender": "Female" }, { "id": 36, "firstName": "Gloria", "lastName": "Johnson", "email": "gjohnsonz@diigo.com", "gender": "Female" }, { "id": 37, "firstName": "Jacqueline", "lastName": "Webb", "email": "jwebb10@prnewswire.com", "gender": "Female" }, { "id": 38, "firstName": "Ralph", "lastName": "Meyer", "email": "rmeyer11@economist.com", "gender": "Male" }, { "id": 39, "firstName": "Fred", "lastName": "Foster", "email": "ffoster12@youku.com", "gender": "Male" }, { "id": 40, "firstName": "Carolyn", "lastName": "Daniels", "email": "cdaniels13@google.pl", "gender": "Female" }, { "id": 41, "firstName": "Jessica", "lastName": "Butler", "email": "jbutler14@bluehost.com", "gender": "Female" }, { "id": 42, "firstName": "Eugene", "lastName": "Perez", "email": "eperez15@theglobeandmail.com", "gender": "Male" }, { "id": 43, "firstName": "Brian", "lastName": "Walker", "email": "bwalker16@elegantthemes.com", "gender": "Male" }, { "id": 44, "firstName": "Walter", "lastName": "Holmes", "email": "wholmes17@hatena.ne.jp", "gender": "Male" }, { "id": 45, "firstName": "Alice", "lastName": "Smith", "email": "asmith18@wikimedia.org", "gender": "Female" }, { "id": 46, "firstName": "Antonio", "lastName": "Myers", "email": "amyers19@omniture.com", "gender": "Male" }, { "id": 47, "firstName": "Joan", "lastName": "Banks", "email": "jbanks1a@trellian.com", "gender": "Female" }, { "id": 48, "firstName": "Gary", "lastName": "Lee", "email": "glee1b@ed.gov", "gender": "Male" }];
}

@NgModule({
  imports: [BrowserModule, ListNavigatorModule],
  declarations: [ App, Widget ],
  bootstrap: [ App ]
})
export class AppModule {}
import { Component, Input, Output, EventEmitter, AfterContentInit, ContentChild, OnChanges, SimpleChanges, SimpleChange } from "@angular/core";
import {ListNavigatorItem} from "./list-navigator-item.directive";
import {analyzeChanges} from "./utils";
import {ListNavigatorItemContext} from "./list-navigator-item-context";

@Component({
    selector: "list-navigator",
    templateUrl: "src/controls/list-navigator/list-navigator.component.html"
})
export class ListNavigator implements AfterContentInit, OnChanges
{
    private _currentOffset: number = 0;

    protected itemsToDisplay: ListNavigatorItemContext[] = [];

    protected get prevEnabled(): boolean {
        return this._currentOffset - this.pageSize >= 0;
    }

    protected nextEnabled: boolean;

    @ContentChild(ListNavigatorItem)
    protected templateOutlet: ListNavigatorItem;

    @Input()
    public dataSource: (offset: number, pageSize: number) => any[];

    @Input()
    public pageSize: number = 5;

    @Input()
    public selectedItems: any[] = [];

    @Output()
    public readonly selectedItemsChange: EventEmitter<any[]> = new EventEmitter<any[]>();

    public ngAfterContentInit(): void {
        if (!this.templateOutlet) {
            console.error("'list-navigator' component has to contain 'list-navigator-item' template directive");
        } else if (!this.templateOutlet.templateRef) {
            console.error("Use '*' in front of 'list-navigator-item' or put 'list-navigator-item' as an attribute into <template> directive");
        }
    }

    public ngOnChanges(changes: SimpleChanges): void {
        const [dsChanged, dsValue] = analyzeChanges(changes, ()=>this.dataSource);
        const [tplChanged, tplOutlet] = analyzeChanges(changes, () => this.templateOutlet);
        const [selectedItemsChanged, selectedItems] = analyzeChanges(changes, () => this.selectedItems);

        if (dsChanged || tplChanged) 
        {
            this.itemsToDisplay = [];
            if (tplOutlet && tplOutlet.templateRef && dsValue) {
                this.onNext(0);
            }
        }

        if (selectedItemsChanged && this.itemsToDisplay && this.itemsToDisplay.length > 0) {
            this.itemsToDisplay.forEach(i => { i.selected = selectedItems.indexOf(i.$implicit) >= 0; });
        }
    }

    protected onPrev() {
        this._currentOffset -= this.pageSize;
        this.itemsToDisplay = this.getItemContexts(this._currentOffset, this.pageSize);
        this.nextEnabled = true;
    }

    protected onNext(forcedOffset?: number) {
        const newOffset = (forcedOffset === null || forcedOffset === undefined)
            ? this._currentOffset + this.pageSize
            : forcedOffset;

        const next = this.getItemContexts(newOffset, this.pageSize + 1);

        this.nextEnabled = false;

        if (next && next.length > 0) {
            this.nextEnabled = next.length === this.pageSize + 1;
            this.itemsToDisplay = next.slice(0, this.pageSize);
            this._currentOffset = newOffset;
        }
    }

    protected onSelectedChange(itemContext: ListNavigatorItemContext, cbValue: boolean) {
        if (cbValue) {
            if (this.selectedItems.indexOf(itemContext.$implicit) < 0) {
                this.selectedItems = Object.assign([], this.selectedItems);
                this.selectedItems.push(itemContext.$implicit);
                this.selectedItemsChange.next(this.selectedItems);
            }
        }
        else {
            const idx = this.selectedItems.indexOf(itemContext.$implicit);

            if (idx >= 0)
            {
                this.selectedItems = Object.assign([], this.selectedItems);
                this.selectedItems.splice(idx, 1);
                this.selectedItemsChange.next(this.selectedItems);
            }            
        }
        itemContext.selected = cbValue;
    }

    private getItemContexts(offset: number, pageSize: number): ListNavigatorItemContext[] {
        return this
            .dataSource(offset, pageSize)
            .map(i => new ListNavigatorItemContext(i, this.selectedItems.indexOf(i) >= 0));
    }
}
<style>
    .cb-wrapper {
        display: inline-block;
    }
    .item-wrapper {
        display: inline-block;
    }
</style>

<div *ngFor="let i of itemsToDisplay">
    <div  class="cb-wrapper">
        <input type="checkbox" [ngModel]="i.selected" (ngModelChange)="onSelectedChange(i, $event)"/>
    </div>
    <div  class="item-wrapper">
        <list-navigator-item-outlet [template]="templateOutlet.templateRef" [context]="i"></list-navigator-item-outlet>
    </div>
</div>
<div>
    <button (click)="onPrev()" [disabled]="!prevEnabled">Prev</button>
    <button (click)="onNext()" [disabled]="!nextEnabled">Next</button>
</div>
export class ListNavigatorItemContext
{
    constructor(
        public $implicit: any,
        public selected: boolean
    ) { }
}
import { Directive, Input, TemplateRef, ViewContainerRef, ContentChild, OnChanges, SimpleChanges } from "@angular/core";
import { ListNavigatorItemContext } from "./list-navigator-item-context";
import {analyzeChanges} from "./utils";

@Directive({
    selector: "list-navigator-item-outlet"
})
export class ListNavigatorItemOutlet
{
    constructor(private readonly _viewContainer: ViewContainerRef){}

    @Input()
    public template: TemplateRef<ListNavigatorItemContext>;

    @Input()
    public context: ListNavigatorItemContext;

    public ngOnChanges(changes: SimpleChanges): void
    {
        const [, tmpl] = analyzeChanges(changes, () => this.template);
        const [, ctx] = analyzeChanges(changes, () => this.context);

        if (tmpl && ctx) {
            this._viewContainer.createEmbeddedView(tmpl, ctx);
        }
    }

}
import { Directive, TemplateRef, Optional } from "@angular/core";
import {ListNavigatorItemContext} from "./list-navigator-item-context";

@Directive({
    selector: "[list-navigator-item]"
})
export class ListNavigatorItem {
    constructor(@Optional() public readonly templateRef: TemplateRef<ListNavigatorItemContext>) {
    }
}
import { NgModule} from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from '@angular/forms'
import {ListNavigator} from "./list-navigator.component";
import {ListNavigatorItem} from "./list-navigator-item.directive";
import {ListNavigatorItemOutlet} from "./list-navigator-item-outlet.directive";

@NgModule({
    imports: [CommonModule, FormsModule],
    declarations: [ListNavigator, ListNavigatorItem, ListNavigatorItemOutlet],
    exports: [ListNavigator, ListNavigatorItem]
})
export class ListNavigatorModule {
    
}
import { SimpleChanges, SimpleChange} from "@angular/core";

export function analyzeChanges<T>(changes: SimpleChanges, defValue: () => T): [boolean, T, T]
{
    const regExp = new RegExp("_this\.(.+?);");
    const match: RegExpExecArray = regExp.exec(defValue.toString());
    if (match.length < 2)
    {
        throw new Error("Could not find property name");
    }
    const propertyName = match[1];

    const simpleChange: SimpleChange = changes[propertyName];

    if (simpleChange != undefined)
    {
        return [true, <T>(simpleChange.currentValue), simpleChange.isFirstChange() ? undefined : simpleChange.previousValue];
    }
    return [false, defValue(), defValue()];
}
import {Component, Input, Output, EventEmitter, AfterContentInit, ContentChild, OnChanges, SimpleChanges, SimpleChange } from "@angular/core";
import {ListNavigatorItem} from "./list-navigator-item.directive";
import {analyzeChanges} from "./utils";
import {ListNavigatorItemContext} from "./list-navigator-item-context";

@Component({
    selector: "widget",
    template: `
<style>
  :host{
    display: block;
    border: 1px solid gray;
    width: 200px;
    height: 200px;
  }
  .wid_btn{
    float: right;
  }
  
  .hdr{
    border-bottom: 1px solid gray;
    padding: 10px;
  }
  
  .cnt{
    padding: 10px;
  }
</style>
  <div class="hdr">
    <span>Some widget</span>
    <button *ngIf="!settingMode" (click)="settingMode = true" class="wid_btn">
      Settings
    </button>
    <button *ngIf="settingMode" (click)="settingMode = false" class="wid_btn">
      Ok
    </button>
  </div>
  <div class="cnt">
    <ng-content *ngIf="!settingMode" select=".content">
    </ng-content>
    <div *ngIf="settingMode">
      Settings:
      <ng-content select=".settings">
      </ng-content>
    </div>
  <div>    
    `})
export class Widget {
  protected settingMode: boolean = false;
}