From 1e4095d4defa377ace7d816c39bbaf1d09c69666 Mon Sep 17 00:00:00 2001 From: TMaster Date: Sun, 22 Oct 2017 20:53:18 +0300 Subject: [PATCH] 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 --- src/tree.component.ts | 7 +++ src/tree.events.ts | 6 +++ src/tree.service.ts | 28 +++++++++- src/tree.ts | 12 +++-- src/tree.types.ts | 3 +- test/tree.service.spec.ts | 110 ++++++++++++++++++++++++++++++++++++++ test/tree.spec.ts | 28 ++++++++++ 7 files changed, 186 insertions(+), 8 deletions(-) diff --git a/src/tree.component.ts b/src/tree.component.ts index 4f1dd2e5..6dd7d508 100644 --- a/src/tree.component.ts +++ b/src/tree.component.ts @@ -46,6 +46,9 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { @Output() public nodeCollapsed: EventEmitter = new EventEmitter(); +@Output() +public loadNextLevel: EventEmitter = new EventEmitter(); + public tree: Tree; @ViewChild('rootComponent') public rootComponent; @@ -90,6 +93,10 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { this.subscriptions.push(this.treeService.nodeCollapsed$.subscribe((e: NodeEvent) => { this.nodeCollapsed.emit(e); })); + + this.subscriptions.push(this.treeService.loadNextLevel$.subscribe((e: NodeEvent) => { + this.loadNextLevel.emit(e); + })); } public getController(): TreeController { diff --git a/src/tree.events.ts b/src/tree.events.ts index 9a7baf93..ac546b68 100644 --- a/src/tree.events.ts +++ b/src/tree.events.ts @@ -54,3 +54,9 @@ export class NodeCollapsedEvent extends NodeEvent { super(node); } } + +export class LoadNextLevelEvent extends NodeEvent { + public constructor(node: Tree) { + super(node); + } +} diff --git a/src/tree.service.ts b/src/tree.service.ts index d47bdc05..d8462cd9 100644 --- a/src/tree.service.ts +++ b/src/tree.service.ts @@ -5,7 +5,8 @@ import { NodeMovedEvent, NodeRemovedEvent, NodeRenamedEvent, - NodeSelectedEvent + NodeSelectedEvent, + LoadNextLevelEvent } from './tree.events'; import { RenamableNode } from './tree.types'; import { Tree } from './tree'; @@ -14,6 +15,7 @@ import { Observable, Subject } from 'rxjs/Rx'; import { ElementRef, Inject, Injectable } from '@angular/core'; import { NodeDraggableService } from './draggable/node-draggable.service'; import { NodeDraggableEvent } from './draggable/draggable.events'; +import {isEmpty} from './utils/fn.utils'; @Injectable() export class TreeService { @@ -24,10 +26,11 @@ export class TreeService { public nodeSelected$: Subject = new Subject(); public nodeExpanded$: Subject = new Subject(); public nodeCollapsed$: Subject = new Subject(); + public loadNextLevel$: Subject = new Subject(); private controllers: Map = new Map(); - public constructor(@Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) { + public constructor( @Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) { this.nodeRemoved$.subscribe((e: NodeRemovedEvent) => e.node.removeItselfFromParent()); } @@ -58,6 +61,9 @@ export class TreeService { public fireNodeSwitchFoldingType(tree: Tree): void { if (tree.isNodeExpanded()) { this.fireNodeExpanded(tree); + if (this.shouldFireLoadNextLevel(tree)) { + this.fireLoadNextLevel(tree); + } } else if (tree.isNodeCollapsed()) { this.fireNodeCollapsed(tree); } @@ -71,6 +77,10 @@ export class TreeService { this.nodeCollapsed$.next(new NodeCollapsedEvent(tree)); } + private fireLoadNextLevel(tree: Tree): void { + this.loadNextLevel$.next(new LoadNextLevelEvent(tree)); + } + public draggedStream(tree: Tree, element: ElementRef): Observable { return this.nodeDraggableService.draggableNodeEvents$ .filter((e: NodeDraggableEvent) => e.target === element) @@ -98,4 +108,18 @@ export class TreeService { public hasController(id: string | number): boolean { return this.controllers.has(id); } + + private shouldFireLoadNextLevel(tree: Tree): boolean { + + const shouldLoadNextLevel = tree.node.emitLoadNextLevel && + !tree.node.loadChildren && + !tree.childrenAreBeingLoaded() && + (!tree.children || isEmpty(tree.children)); + + if (shouldLoadNextLevel) { + tree.loadingChildrenRequested(); + } + + return shouldLoadNextLevel; + } } diff --git a/src/tree.ts b/src/tree.ts index d8cafcf6..8d71b2f7 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -24,7 +24,6 @@ export class Tree { private _children: Tree[]; private _loadChildren: ChildrenLoadingFunction; private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted; - private _childrenAsyncOnce: () => Observable = once(() => { return new Observable((observer: Observer) => { setTimeout(() => { @@ -91,7 +90,7 @@ export class Tree { this.parent = parent; this.node = Object.assign(omit(model, 'children') as TreeModel, { settings: TreeModelSettings.merge(model, get(parent, 'node') as TreeModel) - }) as TreeModel; + }, { emitLoadNextLevel: model.emitLoadNextLevel === true }) as TreeModel; if (isFunction(this.node.loadChildren)) { this._loadChildren = this.node.loadChildren; @@ -109,6 +108,10 @@ export class Tree { public hasDeferredChildren(): boolean { return typeof this._loadChildren === 'function'; } + /* Setting the children loading state to Loading since a request was dispatched to the client */ + public loadingChildrenRequested(): void { + this._childrenLoadingState = ChildrenLoadingState.Loading; + } /** * Check whether children of the node are being loaded. @@ -140,7 +143,7 @@ export class Tree { * @returns {boolean} A flag indicating that children should be loaded for the current node. */ public childrenShouldBeLoaded(): boolean { - return !!this._loadChildren; + return !!this._loadChildren || this.node.emitLoadNextLevel === true; } /** @@ -337,7 +340,7 @@ export class Tree { * @returns {boolean} A flag indicating whether or not this tree is a "Branch". */ public isBranch(): boolean { - return Array.isArray(this._children); + return this.node.emitLoadNextLevel === true || Array.isArray(this._children); } /** @@ -411,7 +414,6 @@ export class Tree { if (this.isLeaf() || !this.hasChildren()) { return; } - this.node._foldingType = this.isNodeExpanded() ? FoldingType.Collapsed : FoldingType.Expanded; } diff --git a/src/tree.types.ts b/src/tree.types.ts index f63719ae..e0c3ba29 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -22,9 +22,10 @@ export interface TreeModel { children?: TreeModel[]; loadChildren?: ChildrenLoadingFunction; settings?: TreeModelSettings; + emitLoadNextLevel?: boolean; _status?: TreeStatus; _foldingType?: FoldingType; -} + } export interface CssClasses { /* The class or classes that should be added to the expanded node */ diff --git a/test/tree.service.spec.ts b/test/tree.service.spec.ts index 5bea1dec..cbebedd0 100644 --- a/test/tree.service.spec.ts +++ b/test/tree.service.spec.ts @@ -255,4 +255,114 @@ describe('TreeService', () => { expect(treeService.nodeCollapsed$.next).toHaveBeenCalled(); expect(treeService.nodeExpanded$.next).not.toHaveBeenCalled(); }); + + it('fires "loadNextLevel" event when expanding node with hasChildren property set to true', () => { + const masterTree = new Tree({ + value: 'Master', + emitLoadNextLevel: true + }); + + masterTree.switchFoldingType(); + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).toHaveBeenCalled(); + }); + + it('fires "loadNextLevel" only once', () => { + const masterTree = new Tree({ + value: 'Master', + emitLoadNextLevel: true + }); + + masterTree.switchFoldingType(); + masterTree.switchFoldingType(); + masterTree.switchFoldingType(); + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).toHaveBeenCalledTimes(1); + }); + + it('fires "loadNextLevel" if children are provided as empty array', () => { + const masterTree = new Tree({ + value: 'Master', + emitLoadNextLevel: true, + children: [] + }); + + masterTree.switchFoldingType(); + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).toHaveBeenCalled(); + }); + + it('not fires "loadNextLevel" if "loadChildren" function is provided', () => { + const masterTree = new Tree({ + value: 'Master', + emitLoadNextLevel: true, + loadChildren: (callback) => { + setTimeout(() => { + callback([ + { value: '1' }, + { value: '2' }, + { value: '3' } + ]); + + }); + } + }); + + masterTree.switchFoldingType(); + + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled(); + }); + + it('not fires "loadNextLevel" if children are provided', () => { + const masterTree = new Tree({ + value: 'Master', + emitLoadNextLevel: true, + children: [ + { value: '1' }, + { value: '2' }, + { value: '3' } + ] + }); + + masterTree.switchFoldingType(); + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled(); + }); + + it('not fires "loadNextLevel" event if "hasChildren" is false or does not exists', () => { + const masterTree = new Tree({ + value: 'Master', + }); + + masterTree.switchFoldingType(); + + spyOn(treeService.loadNextLevel$, 'next'); + + treeService.fireNodeSwitchFoldingType(masterTree); + + expect(treeService.loadNextLevel$.next).not.toHaveBeenCalled(); + }); + + }); diff --git a/test/tree.spec.ts b/test/tree.spec.ts index f78912e3..260e23d6 100644 --- a/test/tree.spec.ts +++ b/test/tree.spec.ts @@ -1086,4 +1086,32 @@ describe('Tree', () => { expect(masterTree.children[1].leftMenuTemplate).toEqual(''); }); + it('should load children when hasChildren is true', () => { + + const model: TreeModel = { + value: 'root', + emitLoadNextLevel: true, + id: 6, + }; + + const tree: Tree = new Tree(model); + + expect(tree.hasChildren).toBeTruthy(); + expect(tree.childrenShouldBeLoaded()).toBeTruthy(); + + }); + + it('should be considered as a branch if hasChildren is true', () => { + + const model: TreeModel = { + value: 'root', + emitLoadNextLevel: true, + id: 6, + }; + + const tree: Tree = new Tree(model); + + expect(tree.isBranch()).toBeTruthy(); + }); + });