Skip to content

Commit 1e4095d

Browse files
TmasterGeorgii Rychko
authored andcommitted
feat(*): support ngrx (or loading children using any other redux-like library via special LoadNextLevel event)
* Allow expanding the node once. * Support hasChildren property * Removing console.log * Exposing new LoadNextLevel event * Adding tests for tree.ts * Adding tests for tree-service * Formatting code * Refactor code for PR * Formatting code
1 parent 79c3a83 commit 1e4095d

File tree

7 files changed

+186
-8
lines changed

7 files changed

+186
-8
lines changed

src/tree.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy {
4646
@Output()
4747
public nodeCollapsed: EventEmitter<any> = new EventEmitter();
4848

49+
@Output()
50+
public loadNextLevel: EventEmitter<any> = new EventEmitter();
51+
4952
public tree: Tree;
5053
@ViewChild('rootComponent') public rootComponent;
5154

@@ -90,6 +93,10 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy {
9093
this.subscriptions.push(this.treeService.nodeCollapsed$.subscribe((e: NodeEvent) => {
9194
this.nodeCollapsed.emit(e);
9295
}));
96+
97+
this.subscriptions.push(this.treeService.loadNextLevel$.subscribe((e: NodeEvent) => {
98+
this.loadNextLevel.emit(e);
99+
}));
93100
}
94101

95102
public getController(): TreeController {

src/tree.events.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,9 @@ export class NodeCollapsedEvent extends NodeEvent {
5454
super(node);
5555
}
5656
}
57+
58+
export class LoadNextLevelEvent extends NodeEvent {
59+
public constructor(node: Tree) {
60+
super(node);
61+
}
62+
}

src/tree.service.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
NodeMovedEvent,
66
NodeRemovedEvent,
77
NodeRenamedEvent,
8-
NodeSelectedEvent
8+
NodeSelectedEvent,
9+
LoadNextLevelEvent
910
} from './tree.events';
1011
import { RenamableNode } from './tree.types';
1112
import { Tree } from './tree';
@@ -14,6 +15,7 @@ import { Observable, Subject } from 'rxjs/Rx';
1415
import { ElementRef, Inject, Injectable } from '@angular/core';
1516
import { NodeDraggableService } from './draggable/node-draggable.service';
1617
import { NodeDraggableEvent } from './draggable/draggable.events';
18+
import {isEmpty} from './utils/fn.utils';
1719

1820
@Injectable()
1921
export class TreeService {
@@ -24,10 +26,11 @@ export class TreeService {
2426
public nodeSelected$: Subject<NodeSelectedEvent> = new Subject<NodeSelectedEvent>();
2527
public nodeExpanded$: Subject<NodeExpandedEvent> = new Subject<NodeExpandedEvent>();
2628
public nodeCollapsed$: Subject<NodeCollapsedEvent> = new Subject<NodeCollapsedEvent>();
29+
public loadNextLevel$: Subject<LoadNextLevelEvent> = new Subject<LoadNextLevelEvent>();
2730

2831
private controllers: Map<string | number, TreeController> = new Map();
2932

30-
public constructor(@Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) {
33+
public constructor( @Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) {
3134
this.nodeRemoved$.subscribe((e: NodeRemovedEvent) => e.node.removeItselfFromParent());
3235
}
3336

@@ -58,6 +61,9 @@ export class TreeService {
5861
public fireNodeSwitchFoldingType(tree: Tree): void {
5962
if (tree.isNodeExpanded()) {
6063
this.fireNodeExpanded(tree);
64+
if (this.shouldFireLoadNextLevel(tree)) {
65+
this.fireLoadNextLevel(tree);
66+
}
6167
} else if (tree.isNodeCollapsed()) {
6268
this.fireNodeCollapsed(tree);
6369
}
@@ -71,6 +77,10 @@ export class TreeService {
7177
this.nodeCollapsed$.next(new NodeCollapsedEvent(tree));
7278
}
7379

80+
private fireLoadNextLevel(tree: Tree): void {
81+
this.loadNextLevel$.next(new LoadNextLevelEvent(tree));
82+
}
83+
7484
public draggedStream(tree: Tree, element: ElementRef): Observable<NodeDraggableEvent> {
7585
return this.nodeDraggableService.draggableNodeEvents$
7686
.filter((e: NodeDraggableEvent) => e.target === element)
@@ -98,4 +108,18 @@ export class TreeService {
98108
public hasController(id: string | number): boolean {
99109
return this.controllers.has(id);
100110
}
111+
112+
private shouldFireLoadNextLevel(tree: Tree): boolean {
113+
114+
const shouldLoadNextLevel = tree.node.emitLoadNextLevel &&
115+
!tree.node.loadChildren &&
116+
!tree.childrenAreBeingLoaded() &&
117+
(!tree.children || isEmpty(tree.children));
118+
119+
if (shouldLoadNextLevel) {
120+
tree.loadingChildrenRequested();
121+
}
122+
123+
return shouldLoadNextLevel;
124+
}
101125
}

src/tree.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export class Tree {
2424
private _children: Tree[];
2525
private _loadChildren: ChildrenLoadingFunction;
2626
private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted;
27-
2827
private _childrenAsyncOnce: () => Observable<Tree[]> = once(() => {
2928
return new Observable((observer: Observer<Tree[]>) => {
3029
setTimeout(() => {
@@ -91,7 +90,7 @@ export class Tree {
9190
this.parent = parent;
9291
this.node = Object.assign(omit(model, 'children') as TreeModel, {
9392
settings: TreeModelSettings.merge(model, get(parent, 'node') as TreeModel)
94-
}) as TreeModel;
93+
}, { emitLoadNextLevel: model.emitLoadNextLevel === true }) as TreeModel;
9594

9695
if (isFunction(this.node.loadChildren)) {
9796
this._loadChildren = this.node.loadChildren;
@@ -109,6 +108,10 @@ export class Tree {
109108
public hasDeferredChildren(): boolean {
110109
return typeof this._loadChildren === 'function';
111110
}
111+
/* Setting the children loading state to Loading since a request was dispatched to the client */
112+
public loadingChildrenRequested(): void {
113+
this._childrenLoadingState = ChildrenLoadingState.Loading;
114+
}
112115

113116
/**
114117
* Check whether children of the node are being loaded.
@@ -140,7 +143,7 @@ export class Tree {
140143
* @returns {boolean} A flag indicating that children should be loaded for the current node.
141144
*/
142145
public childrenShouldBeLoaded(): boolean {
143-
return !!this._loadChildren;
146+
return !!this._loadChildren || this.node.emitLoadNextLevel === true;
144147
}
145148

146149
/**
@@ -337,7 +340,7 @@ export class Tree {
337340
* @returns {boolean} A flag indicating whether or not this tree is a "Branch".
338341
*/
339342
public isBranch(): boolean {
340-
return Array.isArray(this._children);
343+
return this.node.emitLoadNextLevel === true || Array.isArray(this._children);
341344
}
342345

343346
/**
@@ -411,7 +414,6 @@ export class Tree {
411414
if (this.isLeaf() || !this.hasChildren()) {
412415
return;
413416
}
414-
415417
this.node._foldingType = this.isNodeExpanded() ? FoldingType.Collapsed : FoldingType.Expanded;
416418
}
417419

src/tree.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ export interface TreeModel {
2222
children?: TreeModel[];
2323
loadChildren?: ChildrenLoadingFunction;
2424
settings?: TreeModelSettings;
25+
emitLoadNextLevel?: boolean;
2526
_status?: TreeStatus;
2627
_foldingType?: FoldingType;
27-
}
28+
}
2829

2930
export interface CssClasses {
3031
/* The class or classes that should be added to the expanded node */

test/tree.service.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,114 @@ describe('TreeService', () => {
255255
expect(treeService.nodeCollapsed$.next).toHaveBeenCalled();
256256
expect(treeService.nodeExpanded$.next).not.toHaveBeenCalled();
257257
});
258+
259+
it('fires "loadNextLevel" event when expanding node with hasChildren property set to true', () => {
260+
const masterTree = new Tree({
261+
value: 'Master',
262+
emitLoadNextLevel: true
263+
});
264+
265+
masterTree.switchFoldingType();
266+
267+
spyOn(treeService.loadNextLevel$, 'next');
268+
269+
treeService.fireNodeSwitchFoldingType(masterTree);
270+
271+
expect(treeService.loadNextLevel$.next).toHaveBeenCalled();
272+
});
273+
274+
it('fires "loadNextLevel" only once', () => {
275+
const masterTree = new Tree({
276+
value: 'Master',
277+
emitLoadNextLevel: true
278+
});
279+
280+
masterTree.switchFoldingType();
281+
masterTree.switchFoldingType();
282+
masterTree.switchFoldingType();
283+
284+
spyOn(treeService.loadNextLevel$, 'next');
285+
286+
treeService.fireNodeSwitchFoldingType(masterTree);
287+
288+
expect(treeService.loadNextLevel$.next).toHaveBeenCalledTimes(1);
289+
});
290+
291+
it('fires "loadNextLevel" if children are provided as empty array', () => {
292+
const masterTree = new Tree({
293+
value: 'Master',
294+
emitLoadNextLevel: true,
295+
children: []
296+
});
297+
298+
masterTree.switchFoldingType();
299+
300+
spyOn(treeService.loadNextLevel$, 'next');
301+
302+
treeService.fireNodeSwitchFoldingType(masterTree);
303+
304+
expect(treeService.loadNextLevel$.next).toHaveBeenCalled();
305+
});
306+
307+
it('not fires "loadNextLevel" if "loadChildren" function is provided', () => {
308+
const masterTree = new Tree({
309+
value: 'Master',
310+
emitLoadNextLevel: true,
311+
loadChildren: (callback) => {
312+
setTimeout(() => {
313+
callback([
314+
{ value: '1' },
315+
{ value: '2' },
316+
{ value: '3' }
317+
]);
318+
319+
});
320+
}
321+
});
322+
323+
masterTree.switchFoldingType();
324+
325+
326+
spyOn(treeService.loadNextLevel$, 'next');
327+
328+
treeService.fireNodeSwitchFoldingType(masterTree);
329+
330+
expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled();
331+
});
332+
333+
it('not fires "loadNextLevel" if children are provided', () => {
334+
const masterTree = new Tree({
335+
value: 'Master',
336+
emitLoadNextLevel: true,
337+
children: [
338+
{ value: '1' },
339+
{ value: '2' },
340+
{ value: '3' }
341+
]
342+
});
343+
344+
masterTree.switchFoldingType();
345+
346+
spyOn(treeService.loadNextLevel$, 'next');
347+
348+
treeService.fireNodeSwitchFoldingType(masterTree);
349+
350+
expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled();
351+
});
352+
353+
it('not fires "loadNextLevel" event if "hasChildren" is false or does not exists', () => {
354+
const masterTree = new Tree({
355+
value: 'Master',
356+
});
357+
358+
masterTree.switchFoldingType();
359+
360+
spyOn(treeService.loadNextLevel$, 'next');
361+
362+
treeService.fireNodeSwitchFoldingType(masterTree);
363+
364+
expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled();
365+
});
366+
367+
258368
});

test/tree.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,4 +1086,32 @@ describe('Tree', () => {
10861086
expect(masterTree.children[1].leftMenuTemplate).toEqual('<i class="navigation"></i>');
10871087
});
10881088

1089+
it('should load children when hasChildren is true', () => {
1090+
1091+
const model: TreeModel = {
1092+
value: 'root',
1093+
emitLoadNextLevel: true,
1094+
id: 6,
1095+
};
1096+
1097+
const tree: Tree = new Tree(model);
1098+
1099+
expect(tree.hasChildren).toBeTruthy();
1100+
expect(tree.childrenShouldBeLoaded()).toBeTruthy();
1101+
1102+
});
1103+
1104+
it('should be considered as a branch if hasChildren is true', () => {
1105+
1106+
const model: TreeModel = {
1107+
value: 'root',
1108+
emitLoadNextLevel: true,
1109+
id: 6,
1110+
};
1111+
1112+
const tree: Tree = new Tree(model);
1113+
1114+
expect(tree.isBranch()).toBeTruthy();
1115+
});
1116+
10891117
});

0 commit comments

Comments
 (0)