Skip to content

Commit

Permalink
feat(*): support ngrx (or loading children using any other redux-like…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
Tmaster authored and Georgii Rychko committed Oct 22, 2017
1 parent 79c3a83 commit 1e4095d
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 8 deletions.
7 changes: 7 additions & 0 deletions src/tree.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy {
@Output()
public nodeCollapsed: EventEmitter<any> = new EventEmitter();

@Output()
public loadNextLevel: EventEmitter<any> = new EventEmitter();

public tree: Tree;
@ViewChild('rootComponent') public rootComponent;

Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/tree.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ export class NodeCollapsedEvent extends NodeEvent {
super(node);
}
}

export class LoadNextLevelEvent extends NodeEvent {
public constructor(node: Tree) {
super(node);
}
}
28 changes: 26 additions & 2 deletions src/tree.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
NodeMovedEvent,
NodeRemovedEvent,
NodeRenamedEvent,
NodeSelectedEvent
NodeSelectedEvent,
LoadNextLevelEvent
} from './tree.events';
import { RenamableNode } from './tree.types';
import { Tree } from './tree';
Expand All @@ -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 {
Expand All @@ -24,10 +26,11 @@ export class TreeService {
public nodeSelected$: Subject<NodeSelectedEvent> = new Subject<NodeSelectedEvent>();
public nodeExpanded$: Subject<NodeExpandedEvent> = new Subject<NodeExpandedEvent>();
public nodeCollapsed$: Subject<NodeCollapsedEvent> = new Subject<NodeCollapsedEvent>();
public loadNextLevel$: Subject<LoadNextLevelEvent> = new Subject<LoadNextLevelEvent>();

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

public constructor(@Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) {
public constructor( @Inject(NodeDraggableService) private nodeDraggableService: NodeDraggableService) {
this.nodeRemoved$.subscribe((e: NodeRemovedEvent) => e.node.removeItselfFromParent());
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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<NodeDraggableEvent> {
return this.nodeDraggableService.draggableNodeEvents$
.filter((e: NodeDraggableEvent) => e.target === element)
Expand Down Expand Up @@ -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;
}
}
12 changes: 7 additions & 5 deletions src/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export class Tree {
private _children: Tree[];
private _loadChildren: ChildrenLoadingFunction;
private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted;

private _childrenAsyncOnce: () => Observable<Tree[]> = once(() => {
return new Observable((observer: Observer<Tree[]>) => {
setTimeout(() => {
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -411,7 +414,6 @@ export class Tree {
if (this.isLeaf() || !this.hasChildren()) {
return;
}

this.node._foldingType = this.isNodeExpanded() ? FoldingType.Collapsed : FoldingType.Expanded;
}

Expand Down
3 changes: 2 additions & 1 deletion src/tree.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
110 changes: 110 additions & 0 deletions test/tree.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});


});
28 changes: 28 additions & 0 deletions test/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,4 +1086,32 @@ describe('Tree', () => {
expect(masterTree.children[1].leftMenuTemplate).toEqual('<i class="navigation"></i>');
});

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();
});

});

0 comments on commit 1e4095d

Please sign in to comment.