Skip to content

Commit bbbb8f7

Browse files
committed
feat(tree): add support of async children loading on node expand
1 parent 45d619f commit bbbb8f7

11 files changed

Lines changed: 510 additions & 296 deletions

TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Cover with protractor e2e test async children loading
2+
- Document `Tree` class

demo/app.component.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,22 @@ export class AppComponent implements OnInit {
9999
]
100100
},
101101
{
102-
value: 'Monospace',
103-
children: [
104-
{ value: 'Input Mono' },
105-
{ value: 'Roboto Mono' },
106-
{ value: 'Liberation Mono' },
107-
{ value: 'Hack' },
108-
{ value: 'Consolas' },
109-
{ value: 'Menlo' },
110-
{ value: 'Source Code Pro' }
111-
]
102+
value: 'Monospace - With ASYNC CHILDREN',
103+
// children property is ignored if "loadChildren" is present
104+
children: [{value: 'I am the font that will be ignored'}],
105+
loadChildren: (callback) => {
106+
setTimeout(() => {
107+
callback([
108+
{ value: 'Input Mono' },
109+
{ value: 'Roboto Mono' },
110+
{ value: 'Liberation Mono' },
111+
{ value: 'Hack' },
112+
{ value: 'Consolas' },
113+
{ value: 'Menlo' },
114+
{ value: 'Source Code Pro' }
115+
]);
116+
}, 5000);
117+
}
112118
}
113119
]
114120
};

src/styles.css

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,30 @@ tree-internal ul.rootless {
150150

151151
tree-internal div.rootless {
152152
display: none !important;
153-
}
153+
}
154+
155+
tree-internal .loading-children:after {
156+
content: ' loading ...';
157+
color: #6a1b9a;
158+
font-style: italic;
159+
font-size: 0.9em;
160+
animation-name: loading-children;
161+
animation-duration: 2s;
162+
animation-timing-function: ease-in-out;
163+
animation-iteration-count: infinite;
164+
}
165+
166+
@keyframes loading-children {
167+
0% { color: #f3e5f5; }
168+
12.5% { color: #e1bee7; }
169+
25% { color: #ce93d8; }
170+
37.5% { color: #ba68c8; }
171+
50% { color: #ab47bc; }
172+
62.5% { color: #9c27b0; }
173+
75% { color: #8e24aa; }
174+
87.5% { color: #7b1fa2; }
175+
100% { color: #6a1b9a; }
176+
}
177+
178+
179+

src/tree-internal.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NodeEditableEvent, NodeEditableEventAction } from './editable/editable.
77
import { TreeService } from './tree.service';
88
import * as EventUtils from './utils/event.utils';
99
import { NodeDraggableEvent } from './draggable/draggable.events';
10+
import { Observable } from 'rxjs';
1011

1112
@Component({
1213
selector: 'tree-internal',
@@ -23,7 +24,9 @@ import { NodeDraggableEvent } from './draggable/draggable.events';
2324
<div class="node-value"
2425
*ngIf="!shouldShowInputForTreeValue()"
2526
[class.node-selected]="isSelected"
26-
(click)="onNodeSelected($event)">{{tree.value}}</div>
27+
(click)="onNodeSelected($event)">
28+
{{tree.value}}<span class="loading-children" *ngIf="tree.childrenAreBeingLoaded()"></span>
29+
</div>
2730
2831
<input type="text" class="node-value"
2932
*ngIf="shouldShowInputForTreeValue()"
@@ -34,7 +37,7 @@ import { NodeDraggableEvent } from './draggable/draggable.events';
3437
<node-menu *ngIf="isMenuVisible" (menuItemSelected)="onMenuItemSelected($event)"></node-menu>
3538
3639
<template [ngIf]="tree.isNodeExpanded()">
37-
<tree-internal *ngFor="let child of tree.children" [tree]="child"></tree-internal>
40+
<tree-internal *ngFor="let child of tree.childrenAsync | async" [tree]="child"></tree-internal>
3841
</template>
3942
</li>
4043
</ul>
@@ -48,7 +51,7 @@ export class TreeInternalComponent implements OnInit {
4851
public settings: Ng2TreeSettings;
4952

5053
public isSelected: boolean = false;
51-
private isMenuVisible: boolean = false;
54+
public isMenuVisible: boolean = false;
5255

5356
public constructor(@Inject(NodeMenuService) private nodeMenuService: NodeMenuService,
5457
@Inject(TreeService) private treeService: TreeService,

src/tree.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class TreeComponent implements OnInit, OnChanges {
4444
if (!this.treeModel) {
4545
this.tree = TreeComponent.EMPTY_TREE;
4646
} else {
47-
this.tree = Tree.buildTreeFromModel(this.treeModel);
47+
this.tree = new Tree(this.treeModel);
4848
}
4949
}
5050

src/tree.ts

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,62 @@
11
import * as _ from 'lodash';
2-
import { TreeModel, RenamableNode, FoldingType, TreeStatus, TreeModelSettings } from './tree.types';
2+
import { Observable, Observer } from 'rxjs';
3+
import { TreeModel, RenamableNode, FoldingType, TreeStatus, TreeModelSettings, ChildrenLoadingFunction } from './tree.types';
4+
5+
enum ChildrenLoadingState {
6+
NotStarted,
7+
Loading,
8+
Completed
9+
}
310

411
export class Tree {
512
private _children: Tree[];
13+
private _loadChildren: ChildrenLoadingFunction;
14+
private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted;
15+
public node: TreeModel;
16+
public parent: Tree;
617

7-
public constructor(public node: TreeModel, public parent: Tree = null, isBranch: boolean = false) {
8-
this._children = isBranch ? [] : null;
18+
public constructor(node: TreeModel, parent: Tree = null, isBranch: boolean = false) {
19+
this.buildTreeFromModel(node, parent, isBranch);
20+
}
21+
22+
/**
23+
* Build an instance of Tree from an object implementing TreeModel interface.
24+
* @param {TreeModel} model - A model that is used to build a tree.
25+
* @param {Tree} [parent] - An optional parent if you want to build a tree from the model that should be a child of an existing Tree instance.
26+
* @param {boolean} [isBranch] - An option that makes a branch from created tree. Branch can have children.
27+
* @static
28+
*/
29+
private buildTreeFromModel(model: TreeModel, parent: Tree, isBranch: boolean): void {
30+
this.parent = parent;
31+
this.node = _.extend(_.omit(model, 'children') as TreeModel, {
32+
settings: TreeModelSettings.merge(model, _.get(parent, 'node') as TreeModel)
33+
}) as TreeModel;
34+
35+
if (_.isFunction(this.node.loadChildren)) {
36+
this._loadChildren = this.node.loadChildren;
37+
} else {
38+
_.forEach(_.get(model, 'children') as TreeModel[], (child: TreeModel, index: number) => {
39+
this._addChild(new Tree(child, this), index);
40+
});
41+
}
42+
43+
if (!Array.isArray(this._children)) {
44+
this._children = this.node.loadChildren || isBranch ? [] : null;
45+
}
46+
}
47+
48+
public childrenAreBeingLoaded(): boolean {
49+
return (this._childrenLoadingState === ChildrenLoadingState.Loading);
50+
}
51+
52+
private canLoadChildren(): boolean {
53+
return (this._childrenLoadingState === ChildrenLoadingState.NotStarted)
54+
&& (this.foldingType === FoldingType.Expanded)
55+
&& (!!this._loadChildren);
56+
}
57+
58+
public childrenShouldBeLoaded(): boolean {
59+
return !!this._loadChildren;
960
}
1061

1162
/**
@@ -16,6 +67,22 @@ export class Tree {
1667
return this._children;
1768
}
1869

70+
public get childrenAsync(): Observable<Tree[]> {
71+
if(this.canLoadChildren()) {
72+
setTimeout(() => this._childrenLoadingState = ChildrenLoadingState.Loading);
73+
return new Observable((observer: Observer<Tree[]>) => {
74+
this._loadChildren((children: TreeModel[]) => {
75+
this._children = _.map(children, (child: TreeModel) => new Tree(child, this));
76+
this._childrenLoadingState = ChildrenLoadingState.Completed;
77+
observer.next(this.children);
78+
observer.complete();
79+
});
80+
});
81+
}
82+
83+
return Observable.of(this.children);
84+
}
85+
1986
/**
2087
* Create a new node in the current tree.
2188
* @param {boolean} isBranch - A flag that indicates whether a new node should be a "Branch". "Leaf" node will be created by default
@@ -215,7 +282,9 @@ export class Tree {
215282
*/
216283
public get foldingType(): FoldingType {
217284
if (!this.node._foldingType) {
218-
if (this._children) {
285+
if (this.childrenShouldBeLoaded()) {
286+
this.node._foldingType = FoldingType.Collapsed;
287+
} else if (this._children) {
219288
this.node._foldingType = FoldingType.Expanded;
220289
} else {
221290
this.node._foldingType = FoldingType.Leaf;
@@ -271,24 +340,6 @@ export class Tree {
271340

272341
// STATIC METHODS ----------------------------------------------------------------------------------------------------
273342

274-
/**
275-
* Build an instance of Tree from an object implementing TreeModel interface.
276-
* @param {TreeModel} model - A model that is used to build a tree.
277-
* @param {Tree} [parent] - An optional parent if you want to build a tree from the model that should be a child of an existing Tree instance.
278-
* @returns {Tree} A tree build from given model (with parent if it was given)
279-
* @static
280-
*/
281-
public static buildTreeFromModel(model: TreeModel, parent: Tree = null): Tree {
282-
model.settings = TreeModelSettings.merge(model, _.get(parent, 'node') as TreeModel);
283-
const tree = new Tree(_.omit(model, 'children') as TreeModel, parent);
284-
285-
_.forEach(model.children, (child: TreeModel, index: number) => {
286-
tree._addChild(Tree.buildTreeFromModel(child, tree), index);
287-
});
288-
289-
return tree;
290-
}
291-
292343
/**
293344
* Check that value passed is not empty (it doesn't consist of only whitespace symbols).
294345
* @param {string} value - A value that should be checked.

src/tree.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ export class FoldingType {
1313
}
1414
}
1515

16+
export type ChildrenLoadingFunction = (callback: (children: TreeModel[]) => void) => void;
17+
1618
export interface TreeModel {
1719
value: string | RenamableNode;
1820
children?: TreeModel[];
21+
loadChildren?: ChildrenLoadingFunction;
1922
settings?: TreeModelSettings;
2023
_status?: TreeStatus;
2124
_foldingType?: FoldingType;

test/draggable/node-draggable.service.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('NodeDraggableService', function () {
4242
}));
4343

4444
it('should not fire event if node is static', inject([NodeDraggableService], (nodeDraggableService) => {
45-
const masterTree = Tree.buildTreeFromModel({
45+
const masterTree = new Tree({
4646
value: 'Master',
4747
settings: {
4848
'static': true
@@ -72,4 +72,14 @@ describe('NodeDraggableService', function () {
7272

7373
expect(actualCapturedNode).toBe(stubCapturedNode);
7474
}));
75+
76+
it('should release captured node', inject([NodeDraggableService], (nodeDraggableService) => {
77+
const stubCapturedNode = new CapturedNode(null, null);
78+
79+
nodeDraggableService.captureNode(stubCapturedNode);
80+
expect(nodeDraggableService.getCapturedNode(stubCapturedNode)).toBe(stubCapturedNode);
81+
82+
nodeDraggableService.releaseCapturedNode();
83+
expect(nodeDraggableService.getCapturedNode(stubCapturedNode)).toBeNull();
84+
}));
7585
});

test/tree.service.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('TreeService', () => {
105105
});
106106

107107
it('removes node from parent when when appropriate event fires', done => {
108-
const masterTree = Tree.buildTreeFromModel({
108+
const masterTree = new Tree({
109109
value: 'Master',
110110
children: [
111111
{value: 'Servant#1'},
@@ -127,7 +127,7 @@ describe('TreeService', () => {
127127
});
128128

129129
it('should produce drag event for the same element and not on captured node children', done => {
130-
const masterTree = Tree.buildTreeFromModel({
130+
const masterTree = new Tree({
131131
value: 'Master',
132132
children: [
133133
{value: 'Servant#1'},

0 commit comments

Comments
 (0)