diff --git a/public/docs/_examples/cb-tree-view/e2e-spec.ts b/public/docs/_examples/cb-tree-view/e2e-spec.ts new file mode 100644 index 0000000000..77a3fb033d --- /dev/null +++ b/public/docs/_examples/cb-tree-view/e2e-spec.ts @@ -0,0 +1,65 @@ +/// +'use strict'; +/* tslint:disable:quotemark */ +describe('Tree View', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should expand and collapse all nodes', function(){ + let expandAll = element(by.xpath('//button[text()="Expand All"]')); + let collapseAll = element(by.xpath('//button[text()="Collapse All"]')); + + expandAll.click() + .then(function(){ + expect(findHeroCount('Dr IQ')).toBe(1); + expect(findHeroCount('Bombasto')).toBe(1); + expect(findHeroCount('Celeritas')).toBe(1); + expect(findHeroCount('RubberMan')).toBe(1); + expect(findHeroCount('Tornado')).toBe(1); + expect(findHeroCount('Dynama')).toBe(2); + expect(findHeroCount('Magma')).toBe(1); + }); + + collapseAll.click() + .then(function(){ + expect(findHeroCount('Dr IQ')).toBe(0); + expect(findHeroCount('Bombasto')).toBe(0); + expect(findHeroCount('Celeritas')).toBe(0); + expect(findHeroCount('RubberMan')).toBe(0); + expect(findHeroCount('Tornado')).toBe(0); + expect(findHeroCount('Dynama')).toBe(0); + expect(findHeroCount('Magma')).toBe(0); + }); + }); + + function findHeroCount(name: string) { + let length = element.all(by.xpath('//div[text()="Name: ' + name + '"]')).count(); + return length; + } + + it('should add hero', function () { + let usaNode = element.all(by.xpath('//a[text()="USA"]')).get(0); + + usaNode.click().then(function() { + let name = element(by.xpath('//input[@placeholder="name"]')); + + let rating = element(by.xpath('//input[@placeholder="ranking"]')); + + name.sendKeys('New Hero'); + rating.sendKeys('10'); + + let addButton = element(by.xpath('//button[text()="Add Hero"]')); + return addButton.click(); + }) + .then(function(){ + let name = element(by.xpath('//div[text()="name: New Hero"]')); + expect(name).toBeDefined(); + + let rating = element(by.xpath('//div[text()="10"]')); + expect(rating).toBeDefined(); + }); + }); + +}); diff --git a/public/docs/_examples/cb-tree-view/ts/.gitignore b/public/docs/_examples/cb-tree-view/ts/.gitignore new file mode 100644 index 0000000000..cf44e148ba --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/.gitignore @@ -0,0 +1 @@ +**/*.js \ No newline at end of file diff --git a/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.html b/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.html new file mode 100644 index 0000000000..140fe2ce36 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.html @@ -0,0 +1,7 @@ + + + +
+ + +
diff --git a/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.ts b/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.ts new file mode 100644 index 0000000000..0e89a64360 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/add-hero.component.ts @@ -0,0 +1,30 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; + +import { TreeNodeService } from './tree-node.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'add-hero', + templateUrl: 'app/add-hero.component.html' +}) +export class AddHeroComponent implements OnInit { + hero: Hero; + + constructor(private treeNodeService: TreeNodeService) { } + + addHero(): void { + if (this.hero.name) { + this.treeNodeService.addHero(this.hero); + this.hero = new Hero(); + } + } + + cancel(): void { + this.treeNodeService.selectedNode.unselect(); + } + + ngOnInit(): void { + this.hero = new Hero(); + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/app.component.ts b/public/docs/_examples/cb-tree-view/ts/app/app.component.ts new file mode 100644 index 0000000000..997de30d34 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/app.component.ts @@ -0,0 +1,30 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; + +import { TreeNode } from './tree-node'; +import { TreeNodeService } from './tree-node.service'; + +@Component({ + selector: 'my-app', + template: ` +
+

Hero locations

+ + + +
+ ` +}) +export class AppComponent implements OnInit { + nodes: TreeNode[] = []; + + constructor(private treeNodeService: TreeNodeService) { + } + + ngOnInit() { + this.treeNodeService + .getTreeNodes() + .then((nodes: TreeNode[]) => this.nodes = nodes) + .catch((error: any) => console.log(error)); // TODO: Display error + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/app.module.ts b/public/docs/_examples/cb-tree-view/ts/app/app.module.ts new file mode 100644 index 0000000000..cdd7d47c0d --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/app.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpModule } from '@angular/http'; +import { FormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; +import { AddHeroComponent } from './add-hero.component'; +import { HeroNodeComponent } from './hero-node.component'; +import { TreeViewComponent } from './tree-view.component'; +import { TreeNodeService } from './tree-node.service'; + +@NgModule({ + imports: [ + BrowserModule, + HttpModule, + FormsModule + ], + declarations: [ + AppComponent, + AddHeroComponent, + HeroNodeComponent, + TreeViewComponent + ], + providers: [ TreeNodeService ], + bootstrap: [ AppComponent ] +}) +export class AppModule {} diff --git a/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.html b/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.html new file mode 100644 index 0000000000..4317158265 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.html @@ -0,0 +1,5 @@ + +
+
Name: {{hero.name}}
+
Ranking: {{hero.ranking}}
+
\ No newline at end of file diff --git a/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.ts b/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.ts new file mode 100644 index 0000000000..69bca4d147 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/hero-node.component.ts @@ -0,0 +1,23 @@ +// #docregion +import { Component, Input, OnInit } from '@angular/core'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-node', + templateUrl: 'app/hero-node.component.html' +}) +export class HeroNodeComponent implements OnInit { + @Input() hero: Hero; + heroClass: string; + + ngOnInit(): void { + if (this.hero.ranking > 7) { + this.heroClass = 'hero-top'; + }else if (this.hero.ranking > 4) { + this.heroClass = 'hero-ok'; + }else { + this.heroClass = 'hero-low'; + } + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/hero.ts b/public/docs/_examples/cb-tree-view/ts/app/hero.ts new file mode 100644 index 0000000000..27f31d9ba2 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/hero.ts @@ -0,0 +1,4 @@ +// #docregion +export class Hero { + constructor(public name?: string, public ranking?: number) { } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/main.ts b/public/docs/_examples/cb-tree-view/ts/app/main.ts new file mode 100644 index 0000000000..c950c2584d --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/main.ts @@ -0,0 +1,8 @@ +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app.module'; + +import 'rxjs/Rx'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/cb-tree-view/ts/app/mock-data.json b/public/docs/_examples/cb-tree-view/ts/app/mock-data.json new file mode 100644 index 0000000000..3d2675c411 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/mock-data.json @@ -0,0 +1,25 @@ +{ + "nodes":[ + { + "title":"USA", + "nodes":[ + { + "title":"New York City", + "nodes":[ + {"title":"Brooklyn","heroes":[{"name":"Dr IQ","ranking":10}]}, + {"title":"Manhattan","heroes":[{"name":"Bombasto","ranking":2}]}, + {"title":"Bronx","heroes":[{"name":"Celeritas","ranking":4}]}, + {"title":"Queens","heroes":[{"name":"RubberMan","ranking":6}]}, + {"title":"Staten Island","heroes":[{"name":"Dynama","ranking":8}]}] + } + ] + }, + { + "title":"Canada", + "nodes":[ + {"title":"Calgary","heroes":[{"name":"Tornado","ranking":3},{"name":"Dynama","ranking":8}]}, + {"title":"Ottawa","heroes":[{"name":"Magma","ranking":10}]} + ] + } + ] +} \ No newline at end of file diff --git a/public/docs/_examples/cb-tree-view/ts/app/tree-node.service.ts b/public/docs/_examples/cb-tree-view/ts/app/tree-node.service.ts new file mode 100644 index 0000000000..a819edad26 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/tree-node.service.ts @@ -0,0 +1,59 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; + +import { TreeNode } from './tree-node'; +import { Hero } from './hero'; + +@Injectable() +export class TreeNodeService { + private root = new TreeNode('Hero Regions', [], []); + selectedNode: TreeNode; + + constructor(private http: Http) {} + + // #docregion build-tree + private buildTreeRecursive(root: TreeNode, nodes: any[]): TreeNode { + if (nodes) { + nodes.forEach(node => { + let heroes = (node.heroes || []).map((hero: any) => new Hero(hero.name, hero.ranking)); + let treeNode = new TreeNode(node.title, [], heroes); + root.nodes.push(this.buildTreeRecursive(treeNode, node.nodes)); + return treeNode; + }); + } + return root; + } + + getTreeNodes(): Promise { + return this.http.get('./app/mock-data.json') + .toPromise() + .then(res => { + let root = this.buildTreeRecursive(this.root, res.json().nodes); + return root.nodes; + }); + } + // #enddocregion build-tree + + addHero(newHero: Hero): void { + this.selectedNode.heroes.push(newHero); + } + + // #docregion toggle-nodes + toggleNodes(nodes: TreeNode[], state: boolean): void { + nodes.forEach(node => { + node.expanded = state; + this.toggleNodes(node.nodes, state); + }); + } + // #enddocregion toggle-nodes + + selectNode(node: TreeNode): void { + if (this.selectedNode) { + this.selectedNode.unselect(); + } + + this.selectedNode = node; + this.selectedNode.select(); + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/tree-node.ts b/public/docs/_examples/cb-tree-view/ts/app/tree-node.ts new file mode 100644 index 0000000000..9c415eaab8 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/tree-node.ts @@ -0,0 +1,41 @@ +// #docregion +import { Hero } from './hero'; + +export class TreeNode { + + expanded: boolean = false; + selected: boolean = false; + + constructor(public title: string, public nodes?: TreeNode[], public heroes?: Hero[]) { + } + + getIcon(): string { + if (this.hasChildren()) { + if (this.expanded) { + return '-'; + } + return '+'; + } + return ''; + } + + toggle(): void { + this.expanded = !this.expanded; + if (this.expanded === false) { + this.selected = false; + } + } + + select(): void { + this.selected = true; + this.expanded = true; + } + + unselect(): void { + this.selected = false; + } + + private hasChildren() { + return (this.nodes.length + this.heroes.length) > 0; + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.html b/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.html new file mode 100644 index 0000000000..9aeee4021a --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.html @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.ts b/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.ts new file mode 100644 index 0000000000..d7de1d43ff --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/app/tree-view.component.ts @@ -0,0 +1,19 @@ +// #docregion +import { Component, Input } from '@angular/core'; + +import { TreeNode } from './tree-node'; +import { TreeNodeService } from './tree-node.service'; + +@Component({ + selector: 'tree-view', + templateUrl: './app/tree-view.component.html' +}) +export class TreeViewComponent { + @Input() nodes: Array; + + constructor(private treeNodeService: TreeNodeService) {} + + select(node: TreeNode): void { + this.treeNodeService.selectNode(node); + } +} diff --git a/public/docs/_examples/cb-tree-view/ts/example-config.json b/public/docs/_examples/cb-tree-view/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-tree-view/ts/index.html b/public/docs/_examples/cb-tree-view/ts/index.html new file mode 100644 index 0000000000..8ad0c173e4 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/index.html @@ -0,0 +1,27 @@ + + + + + Tree view + + + + + + + + + + + + + + + + + Loading app... + + + diff --git a/public/docs/_examples/cb-tree-view/ts/plnkr.json b/public/docs/_examples/cb-tree-view/ts/plnkr.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-tree-view/ts/sample.css b/public/docs/_examples/cb-tree-view/ts/sample.css new file mode 100644 index 0000000000..439f3b0cf1 --- /dev/null +++ b/public/docs/_examples/cb-tree-view/ts/sample.css @@ -0,0 +1,56 @@ +ul{ + list-style-type: none; +} +#heroes > ul{ + list-style:none; + padding-left:0; +}​ + +.add-section{ + margin-left: 40px; + margin-bottom: 5px; +} +.add-section input{ + margin-right: 5px; +} +.add-section button{ + margin-right: 5px; + margin-top: 5px; +} +.hero-node{ + color:black; + border: 1px solid black; + margin-bottom: 2px; + padding: 5px; + width: 200px; + border-radius: 3px; +} + +.hero-low{ + background-color: red; +} + +.hero-top{ + background-color: green; +} + +.hero-ok{ + background-color: yellow; +} + +.hero-location{ + color: black; + font-size: 20px; +} + +.tree-node-icon{ + font-weight: bold; + cursor: pointer; + color:black; + font-size: 25px; +} + +.node-selected{ + font-weight: bold; + text-decoration: underline; +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index f82d816cef..56109c2724 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -37,6 +37,11 @@ "basics": true, "hide": true }, + + "tree-view": { + "title": "Tree View", + "intro": "Creating a recursive tree view" + }, "dynamic-form": { "title": "Dynamic Forms", diff --git a/public/docs/ts/latest/cookbook/tree-view.jade b/public/docs/ts/latest/cookbook/tree-view.jade new file mode 100644 index 0000000000..bc9c749b05 --- /dev/null +++ b/public/docs/ts/latest/cookbook/tree-view.jade @@ -0,0 +1,118 @@ +include ../_util-fns + +:marked + Our hero agency has placed heroes in positions all over the world at this point. As an easy way to keep track of heroes by region we have created a tree view. + + In this cookbook we show how to load hero data from an external file and bind it to a recursive template in our tree view component. + + +:marked + ## Table of contents + + [Load Heroes and Regions](#load-heroes-and-regions) + + [Recursive Template](#recursive-template) + + [Adding Heroes](#adding-heroes) + + [Expand/Collapse All](#expand-collapse-all) + +:marked + **See the [live example](/resources/live-examples/cb-tree-view/ts/plnkr.html)**. + +.l-main-section + +:marked + ## Load Heroes and Regions + + We want to load the tree view data from an external file, so first we define a json schema to represent heroes and their regions. A sample of our schema can be found in `mock-data.json`. ++makeExample('cb-tree-view/ts/app/mock-data.json','','app/mock-data.json') + +:marked + In `TreeNodeService` we load the file using `Http` and traverse it recursively to create an object model representing the final tree data. + + Enabling our application to use `Http` requires some configuration, but don't worry we cover the setup in great detail here. ++makeExample('cb-tree-view/ts/app/tree-node.service.ts', 'build-tree', 'app/tree-node.service.ts (building tree)')(format=".") + +:marked + `buildTreeRecursive()` in `TreeNodeService` maps the data from the file into `TreeNode` objects that we bind to the tree view. + + `TreeNode` is the view model for the nodes in the tree and adds support for operations like expand, collapse and selecting of nodes. + ++makeExample('cb-tree-view/ts/app/tree-node.ts','','app/tree-node.ts') + +.l-main-section + +:marked + ## Recursive Template + + To render the tree view we have created `TreeViewComponent`. + ++makeTabs( + `cb-tree-view/ts/app/tree-view.component.ts, + cb-tree-view/ts/app/tree-view.component.html` + , + null, + `app/tree-view.component.ts, + app/tree-view.component.html` +) + +:marked + If we examine `TreeViewComponent` a bit closer we notice that the template refers to itself by specifying a `tree-view` element in the markup. This means the template will render recursively with `TreeViewComponents` inside `TreeViewComponents`. + + The tree is completely data driven by the `TreeNode` view model, and there are no predetermined assumptions about the depth of the tree. +:marked + The final tree view looks like this. +figure.image-display + img(src="/resources/images/cookbooks/tree-view/tree-view.png" alt="Tree-View") + +:marked + In addition to displaying a hero's name under a specific region we also include their ranking. Based on ranking there is a color code for the node. High performing heroes are displayed in green, mid level performers in yellow and heroes who are struggling are displayed in red. + + We are using `HeroNodeComponent` to display the hero nodes. + ++makeTabs( + `cb-tree-view/ts/app/hero-node.component.html, + cb-tree-view/ts/app/hero-node.component.ts`, + null, + `app/hero-node.component.html, + app/hero-node.component.ts` +) + +.l-main-section + +:marked + ## Adding Heroes + + We need the ability to add new heroes to the tree. Selecting a region in the tree will open up the following add hero view. + +figure.image-display + img(src="/resources/images/cookbooks/tree-view/add-hero.png" alt="Add Hero") + +:marked + We have created `AddHeroComponent` as a separate component for adding new heroes. Just type in the new hero's name and ranking and click "Add Hero". + ++makeTabs( + `cb-tree-view/ts/app/add-hero.component.html, + cb-tree-view/ts/app/add-hero.component.ts`, + null, + `app/add-hero.component.html, + app/add-hero.component.ts` +) + +:marked + As we can see, `AddHeroComponent` uses `TreeNodeService` to attach new nodes to the exiting set of nodes. + +.l-main-section + +:marked + ## Expand/Collapse All + + Tree nodes can be expanded or collapsed individually, but out of convenience we want a way to expand or collapse all nodes in a single operation. For that we have added "expand all" and "collapse all" buttons above the tree view. + + The `expanded` flag on `TreeNode` controls the visibility of a node, so to expand or collapse all nodes, we have to recurse over all `TreeNodes` and set the `expanded` flag accordingly. + ++makeExample('cb-tree-view/ts/app/tree-node.service.ts', 'toggle-nodes', 'app/tree-node.service.ts (Toggle nodes)')(format=".") + +:marked + [Back to top](#top) diff --git a/public/resources/images/cookbooks/tree-view/add-hero.png b/public/resources/images/cookbooks/tree-view/add-hero.png new file mode 100644 index 0000000000..608497f0e6 Binary files /dev/null and b/public/resources/images/cookbooks/tree-view/add-hero.png differ diff --git a/public/resources/images/cookbooks/tree-view/tree-view.png b/public/resources/images/cookbooks/tree-view/tree-view.png new file mode 100644 index 0000000000..003addd11b Binary files /dev/null and b/public/resources/images/cookbooks/tree-view/tree-view.png differ