<!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;
}
.btn-arch{
background-color: white;
border: 2px solid lightgray;
float: right;
}
.btn-arch:hover{
border-color: 2px solid gray;
background-color: gainsboro;
}
.item-container {
margin: 8px;
padding: 8px;
width: 450px;
border: 1px solid darkslategrey;
border-radius: 5px;
}
.item-header {
font-size: 1.3rem;
margin-bottom: 3px;
}
.item-header-archived{
color: lightgray;
}
.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;
archived?: boolean;
}
@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" [class.item-header-archived]="i.archived">
{{i.firstName}} {{i.lastName}}
<button *ngIf="!i.archived" class="btn-arch" (click)="onArchive(i)">Archive</button>
</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(", ");
}
protected onArchive(user: User){
user.archived = true;
}
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";
@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;
}