Skip to content

Commit 1540e2f

Browse files
amcdnljosephperrott
authored andcommitted
feat(schematics): tree schematic (#11739)
1 parent 1c67663 commit 1540e2f

File tree

9 files changed

+340
-1
lines changed

9 files changed

+340
-1
lines changed

src/lib/schematics/collection.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@
2828
"description": "Create a component with a responsive sidenav for navigation",
2929
"factory": "./nav/index",
3030
"schema": "./nav/schema.json",
31-
"aliases": [ "material-nav"]
31+
"aliases": [ "material-nav", "materialNav" ]
32+
},
33+
// Create a file tree component
34+
"tree": {
35+
"description": "Create a file tree component.",
36+
"factory": "./tree/index",
37+
"schema": "./tree/schema.json"
3238
},
3339
// Creates a address form component
3440
"addressForm": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.type-icon {
2+
color: #999;
3+
margin-right: 5px;
4+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
2+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
3+
<button mat-icon-button disabled></button>
4+
<mat-icon class="type-icon" [attr.aria-label]="node.type + 'icon'">
5+
{{ node.type === 'file' ? 'description' : 'folder' }}
6+
</mat-icon>
7+
{{node.name}}
8+
</mat-tree-node>
9+
10+
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
11+
<button mat-icon-button matTreeNodeToggle
12+
[attr.aria-label]="'toggle ' + node.name">
13+
<mat-icon class="mat-icon-rtl-mirror">
14+
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
15+
</mat-icon>
16+
</button>
17+
<mat-icon class="type-icon" [attr.aria-label]="node.type + 'icon'">
18+
{{ node.type ==='file' ? 'description' : 'folder' }}
19+
</mat-icon>
20+
{{node.name}}
21+
</mat-tree-node>
22+
</mat-tree>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
import { fakeAsync, ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
5+
6+
describe('<%= classify(name) %>Component', () => {
7+
let component: <%= classify(name) %>Component;
8+
let fixture: ComponentFixture<<%= classify(name) %>Component>;
9+
10+
beforeEach(fakeAsync(() => {
11+
TestBed.configureTestingModule({
12+
declarations: [ <%= classify(name) %>Component ]
13+
})
14+
.compileComponents();
15+
16+
fixture = TestBed.createComponent(<%= classify(name) %>Component);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
}));
20+
21+
it('should compile', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Component<% if (!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if (changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
2+
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
3+
import { of as observableOf } from 'rxjs';
4+
import { FlatTreeControl } from '@angular/cdk/tree';
5+
import { files } from './example-data';
6+
7+
/** File node data with nested structure. */
8+
export interface FileNode {
9+
name: string;
10+
type: string;
11+
children?: FileNode[];
12+
}
13+
14+
/** Flat node with expandable and level information */
15+
export interface TreeNode {
16+
name: string;
17+
type: string;
18+
level: number;
19+
expandable: boolean;
20+
}
21+
22+
@Component({
23+
selector: '<%= selector %>',<% if (inlineTemplate) { %>
24+
template: `
25+
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
26+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
27+
<button mat-icon-button disabled></button>
28+
<mat-icon class="type-icon" [attr.aria-label]="node.type + 'icon'">
29+
{{ node.type ==='file' ? 'description' : 'folder' }}
30+
</mat-icon>
31+
{{node.name}}
32+
</mat-tree-node>
33+
34+
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
35+
<button mat-icon-button matTreeNodeToggle
36+
[attr.aria-label]="'toggle ' + node.name">
37+
<mat-icon class="mat-icon-rtl-mirror">
38+
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
39+
</mat-icon>
40+
</button>
41+
<mat-icon class="type-icon" [attr.aria-label]="node.type + 'icon'">
42+
{{ node.type === 'file' ? 'description' : 'folder' }}
43+
</mat-icon>
44+
{{node.name}}
45+
</mat-tree-node>
46+
</mat-tree>
47+
`,<% } else { %>
48+
templateUrl: './<%= dasherize(name) %>.component.html',<% } if (inlineStyle) { %>
49+
styles: [
50+
`
51+
.type-icon {
52+
color: #999;
53+
margin-right: 5px;
54+
}
55+
`
56+
]<% } else { %>
57+
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if (!!viewEncapsulation) { %>,
58+
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
59+
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
60+
})
61+
export class <%= classify(name) %>Component {
62+
63+
/** The TreeControl controls the expand/collapse state of tree nodes. */
64+
treeControl: FlatTreeControl<TreeNode>;
65+
66+
/** The TreeFlattener is used to generate the flat list of items from hierarchical data. */
67+
treeFlattener: MatTreeFlattener<FileNode, TreeNode>;
68+
69+
/** The MatTreeFlatDataSource connects the control and flattener to provide data. */
70+
dataSource: MatTreeFlatDataSource<FileNode, TreeNode>;
71+
72+
constructor() {
73+
this.treeFlattener = new MatTreeFlattener(
74+
this.transformer,
75+
this.getLevel,
76+
this.isExpandable,
77+
this.getChildren);
78+
79+
this.treeControl = new FlatTreeControl<TreeNode>(this.getLevel, this.isExpandable);
80+
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
81+
this.dataSource.data = files;
82+
}
83+
84+
/** Transform the data to something the tree can read. */
85+
transformer(node: FileNode, level: number) {
86+
return {
87+
name: node.name,
88+
type: node.type,
89+
level: level,
90+
expandable: !!node.children
91+
};
92+
}
93+
94+
/** Get the level of the node */
95+
getLevel(node: TreeNode) {
96+
return node.level;
97+
}
98+
99+
/** Get whether the node is expanded or not. */
100+
isExpandable(node: TreeNode) {
101+
return node.expandable;
102+
};
103+
104+
/** Get the children for the node. */
105+
getChildren(node: FileNode) {
106+
return observableOf(node.children);
107+
}
108+
109+
/** Get whether the node has children or not. */
110+
hasChild(index: number, node: TreeNode){
111+
return node.expandable;
112+
}
113+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/** Example file/folder data. */
2+
export const files = [
3+
{
4+
name: 'material2',
5+
type: 'folder',
6+
children: [
7+
{
8+
name: 'src',
9+
type: 'folder',
10+
children: [
11+
{
12+
name: 'cdk',
13+
children: [
14+
{ name: 'package.json', type: 'file' },
15+
{ name: 'BUILD.bazel', type: 'file' },
16+
]
17+
},
18+
{ name: 'lib', type: 'folder' }
19+
]
20+
}
21+
]
22+
},
23+
{
24+
name: 'angular',
25+
type: 'folder',
26+
children: [
27+
{
28+
name: 'packages',
29+
type: 'folder',
30+
children: [
31+
{ name: '.travis.yml', type: 'file' },
32+
{ name: 'firebase.json', type: 'file' }
33+
]
34+
},
35+
{ name: 'package.json', type: 'file' }
36+
]
37+
},
38+
{
39+
name: 'angularjs',
40+
type: 'folder',
41+
children: [
42+
{ name: 'gulpfile.js', type: 'file' },
43+
{ name: 'README.md', type: 'file' }
44+
]
45+
}
46+
];

src/lib/schematics/tree/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {chain, Rule, noop, Tree, SchematicContext} from '@angular-devkit/schematics';
2+
import {Schema} from './schema';
3+
import {addModuleImportToModule, findModuleFromOptions} from '../utils/ast';
4+
import {buildComponent} from '../utils/devkit-utils/component';
5+
6+
/**
7+
* Scaffolds a new tree component.
8+
* Internally it bootstraps the base component schematic
9+
*/
10+
export default function(options: Schema): Rule {
11+
return chain([
12+
buildComponent({ ...options }),
13+
options.skipImport ? noop() : addTreeModulesToModule(options)
14+
]);
15+
}
16+
17+
/**
18+
* Adds the required modules to the relative module.
19+
*/
20+
function addTreeModulesToModule(options: Schema) {
21+
return (host: Tree) => {
22+
const modulePath = findModuleFromOptions(host, options);
23+
addModuleImportToModule(host, modulePath, 'MatTreeModule', '@angular/material');
24+
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material');
25+
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
26+
return host;
27+
};
28+
}

src/lib/schematics/tree/schema.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsMaterialTree",
4+
"title": "Material Tree Options Schema",
5+
"type": "object",
6+
"properties": {
7+
"path": {
8+
"type": "string",
9+
"format": "path",
10+
"description": "The path to create the component.",
11+
"visible": false
12+
},
13+
"project": {
14+
"type": "string",
15+
"description": "The name of the project.",
16+
"visible": false
17+
},
18+
"name": {
19+
"type": "string",
20+
"description": "The name of the component.",
21+
"$default": {
22+
"$source": "argv",
23+
"index": 0
24+
}
25+
},
26+
"inlineStyle": {
27+
"description": "Specifies if the style will be in the ts file.",
28+
"type": "boolean",
29+
"default": false,
30+
"alias": "s"
31+
},
32+
"inlineTemplate": {
33+
"description": "Specifies if the template will be in the ts file.",
34+
"type": "boolean",
35+
"default": false,
36+
"alias": "t"
37+
},
38+
"viewEncapsulation": {
39+
"description": "Specifies the view encapsulation strategy.",
40+
"enum": ["Emulated", "Native", "None"],
41+
"type": "string",
42+
"alias": "v"
43+
},
44+
"changeDetection": {
45+
"description": "Specifies the change detection strategy.",
46+
"enum": ["Default", "OnPush"],
47+
"type": "string",
48+
"default": "Default",
49+
"alias": "c"
50+
},
51+
"prefix": {
52+
"type": "string",
53+
"format": "html-selector",
54+
"description": "The prefix to apply to generated selectors.",
55+
"alias": "p"
56+
},
57+
"styleext": {
58+
"description": "The file extension to be used for style files.",
59+
"type": "string",
60+
"default": "css"
61+
},
62+
"spec": {
63+
"type": "boolean",
64+
"description": "Specifies if a spec file is generated.",
65+
"default": true
66+
},
67+
"flat": {
68+
"type": "boolean",
69+
"description": "Flag to indicate if a dir is created.",
70+
"default": false
71+
},
72+
"skipImport": {
73+
"type": "boolean",
74+
"description": "Flag to skip the module import.",
75+
"default": false
76+
},
77+
"selector": {
78+
"type": "string",
79+
"format": "html-selector",
80+
"description": "The selector to use for the component."
81+
},
82+
"module": {
83+
"type": "string",
84+
"description": "Allows specification of the declaring module.",
85+
"alias": "m"
86+
},
87+
"export": {
88+
"type": "boolean",
89+
"default": false,
90+
"description": "Specifies if declaring module exports the component."
91+
}
92+
}
93+
}

src/lib/schematics/tree/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {Schema as ComponentSchema} from '@schematics/angular/component/schema';
2+
3+
export interface Schema extends ComponentSchema {}

0 commit comments

Comments
 (0)