Skip to content

Commit ac4f777

Browse files
author
Zhivka Dimova
committed
fix(tree): option to have an empty folder node (no children)
Add new folding type for empty nodes Reset folding type on create or remove of a child Don't create a node on a lazy loading node before children are loaded Expand node when slect to create a children Fix #87
1 parent d4404c4 commit ac4f777

9 files changed

Lines changed: 246 additions & 20 deletions

File tree

demo/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export class AppComponent implements OnInit {
145145
cssClasses: {
146146
expanded: 'fa fa-caret-down',
147147
collapsed: 'fa fa-caret-right',
148+
empty: 'fa fa-caret-right disabled',
148149
leaf: 'fa'
149150
},
150151
templates: {

src/styles.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ tree-internal .tree .folding.node-expanded:before {
144144
color: #757575;
145145
}
146146

147+
tree-internal .tree .folding.node-empty {
148+
color: #212121;
149+
text-align: center;
150+
font-size: 0.89em;
151+
}
152+
153+
tree-internal .tree .folding.node-empty:before {
154+
content: '\25B6';
155+
color: #757575;
156+
}
157+
147158
tree-internal .tree .folding.node-leaf {
148159
color: #212121;
149160
text-align: center;

src/tree.service.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,9 @@ export class TreeService {
5353
}
5454

5555
public fireNodeSwitchFoldingType(tree: Tree): void {
56-
if (tree.isLeaf()) {
57-
return;
58-
}
59-
6056
if (tree.isNodeExpanded()) {
6157
this.fireNodeExpanded(tree);
62-
} else {
58+
} else if (tree.isNodeCollapsed()) {
6359
this.fireNodeCollapsed(tree);
6460
}
6561
}

src/tree.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class Tree {
2222
* @param {boolean} [isBranch] - An option that makes a branch from created tree. Branch can have children.
2323
*/
2424
public constructor(node: TreeModel, parent: Tree = null, isBranch: boolean = false) {
25-
this.buildTreeFromModel(node, parent, isBranch);
25+
this.buildTreeFromModel(node, parent, isBranch || Array.isArray(node.children));
2626
}
2727

2828
private buildTreeFromModel(model: TreeModel, parent: Tree, isBranch: boolean): void {
@@ -53,6 +53,15 @@ export class Tree {
5353
return (this._childrenLoadingState === ChildrenLoadingState.Loading);
5454
}
5555

56+
/**
57+
* Check whether children of the node were loaded.
58+
* Makes sense only for nodes that define `loadChildren` function.
59+
* @returns {boolean} A flag indicating that children were loaded.
60+
*/
61+
public childrenWereLoaded(): boolean {
62+
return (this._childrenLoadingState === ChildrenLoadingState.Completed);
63+
}
64+
5665
private canLoadChildren(): boolean {
5766
return (this._childrenLoadingState === ChildrenLoadingState.NotStarted)
5867
&& (this.foldingType === FoldingType.Expanded)
@@ -111,6 +120,9 @@ export class Tree {
111120
const tree = new Tree({ value: '' }, null, isBranch);
112121
tree.markAsNew();
113122

123+
if (this.childrenShouldBeLoaded() && !(this.childrenAreBeingLoaded() || this.childrenWereLoaded())) {
124+
return null;
125+
}
114126
if (this.isLeaf()) {
115127
return this.addSibling(tree);
116128
} else {
@@ -174,6 +186,11 @@ export class Tree {
174186
} else {
175187
this._children = [child];
176188
}
189+
190+
this._setFoldingType();
191+
if (this.isNodeCollapsed()) {
192+
this.switchFoldingType();
193+
}
177194
return child;
178195
}
179196

@@ -241,6 +258,14 @@ export class Tree {
241258
return Array.isArray(this._children);
242259
}
243260

261+
/**
262+
* Check whether this tree has children.
263+
* @returns {boolean} A flag indicating whether or not this tree has children.
264+
*/
265+
public hasChildren(): boolean {
266+
return !_.isEmpty(this._children) || this.childrenShouldBeLoaded();
267+
}
268+
244269
/**
245270
* Check whether this tree is a root or not. The root is the tree (node) that doesn't have parent (or technically its parent is null).
246271
* @returns {boolean} A flag indicating whether or not this tree is the root.
@@ -278,6 +303,7 @@ export class Tree {
278303
if (childIndex >= 0) {
279304
this._children.splice(childIndex, 1);
280305
}
306+
this._setFoldingType();
281307
}
282308

283309
/**
@@ -296,7 +322,7 @@ export class Tree {
296322
* If node is a "Branch" and it is expanded, then by invoking current method state of the tree should be switched to "collapsed" and vice versa.
297323
*/
298324
public switchFoldingType(): void {
299-
if (this.isLeaf()) {
325+
if (this.isLeaf() || !this.hasChildren()) {
300326
return;
301327
}
302328

@@ -305,20 +331,30 @@ export class Tree {
305331

306332
/**
307333
* Check that tree is expanded.
308-
* @returns {boolean} A flag indicating whether current tree is expanded. Always returns false for the "Leaf" tree.
334+
* @returns {boolean} A flag indicating whether current tree is expanded. Always returns false for the "Leaf" tree and for an empty tree.
309335
*/
310336
public isNodeExpanded(): boolean {
311337
return this.foldingType === FoldingType.Expanded;
312338
}
313339

340+
/**
341+
* Check that tree is collapsed.
342+
* @returns {boolean} A flag indicating whether current tree is collapsed. Always returns false for the "Leaf" tree and for an empty tree.
343+
*/
344+
public isNodeCollapsed(): boolean {
345+
return this.foldingType === FoldingType.Collapsed;
346+
}
347+
314348
/**
315349
* Set a current folding type: expanded, collapsed or leaf.
316350
*/
317351
private _setFoldingType(): void {
318352
if (this.childrenShouldBeLoaded()) {
319353
this.node._foldingType = FoldingType.Collapsed;
320-
} else if (this._children) {
354+
} else if (this._children && !_.isEmpty(this._children)) {
321355
this.node._foldingType = FoldingType.Expanded;
356+
} else if (Array.isArray(this._children)) {
357+
this.node._foldingType = FoldingType.Empty;
322358
} else {
323359
this.node._foldingType = FoldingType.Leaf;
324360
}
@@ -352,6 +388,8 @@ export class Tree {
352388
return _.get(this.node.settings, 'cssClasses.collapsed', null);
353389
} else if (this.node._foldingType === FoldingType.Expanded) {
354390
return _.get(this.node.settings, 'cssClasses.expanded', null);
391+
} else if (this.node._foldingType === FoldingType.Empty) {
392+
return _.get(this.node.settings, 'cssClasses.empty', null);
355393
}
356394

357395
return _.get(this.node.settings, 'cssClasses.leaf', null);

src/tree.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as _ from 'lodash';
33
export class FoldingType {
44
public static Expanded: FoldingType = new FoldingType('node-expanded');
55
public static Collapsed: FoldingType = new FoldingType('node-collapsed');
6+
public static Empty: FoldingType = new FoldingType('node-empty');
67
public static Leaf: FoldingType = new FoldingType('node-leaf');
78

89
public constructor(private _cssClass: string) {
@@ -32,6 +33,9 @@ export interface CssClasses {
3233
/* The class or classes that should be added to the collapsed node */
3334
collapsed?: string;
3435

36+
/* The class or classes that should be added to the empty node */
37+
empty?: string;
38+
3539
/* The class or classes that should be added to the expanded to the leaf */
3640
leaf?: string;
3741
}

test/data-provider/tree.data-provider.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,33 @@ export class TreeDataProvider {
1717
},
1818
'first expanded property of cssClasses has higher priority': {
1919
treeModelA: { value: "12", settings: { cssClasses: { expanded: 'arrow-down-o' } } },
20-
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', leaf: 'dot' } } },
21-
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right', leaf: 'dot' } }
20+
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } },
21+
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } }
2222
},
2323
'first collapsed property of cssClasses has higher priority': {
2424
treeModelA: { value: "12", settings: { cssClasses: { collapsed: 'arrow-right-o' } } },
25-
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', leaf: 'dot' } } },
26-
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right-o', leaf: 'dot' } }
25+
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } },
26+
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right-o', empty: 'arrow-gray', leaf: 'dot' } }
27+
},
28+
'first empty property of cssClasses has higher priority': {
29+
treeModelA: { value: "12", settings: { cssClasses: { empty: 'arrow-gray-o' } } },
30+
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } },
31+
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray-o', leaf: 'dot' } }
2732
},
2833
'first leaf property of cssClasses has higher priority': {
2934
treeModelA: { value: "12", settings: { cssClasses: { leaf: 'dot-o' } } },
30-
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', leaf: 'dot' } } },
31-
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', leaf: 'dot-o' } }
35+
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } },
36+
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot-o' } }
3237
},
3338
'first properties of cssClasses has higher priority': {
34-
treeModelA: { value: "12", settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', leaf: 'dot-o' } } },
35-
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', leaf: 'dot' } } },
36-
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', leaf: 'dot-o' } }
39+
treeModelA: { value: "12", settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } },
40+
treeModelB: { value: "42", settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } },
41+
result: { static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } }
3742
},
3843
'second properties of cssClasses in settings has priority, if first source doesn\'t have them': {
3944
treeModelA: { value: '42', settings: { static: true, leftMenu: true, rightMenu: false } },
40-
treeModelB: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', leaf: 'dot-o' } } },
41-
result: { static: true, leftMenu: true, rightMenu: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', leaf: 'dot-o' } }
45+
treeModelB: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } },
46+
result: { static: true, leftMenu: true, rightMenu: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } }
4247
},
4348
'first node property of templates has higher priority': {
4449
treeModelA: { value: "12", settings: { templates: { node: '<i class="folder-o"></i>' } } },

test/tree.service.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ describe('TreeService', () => {
189189
expect(treeService.nodeCollapsed$.next).not.toHaveBeenCalled();
190190
});
191191

192+
it('does not fire "expanded", "collapsed" events for a empty node', () => {
193+
const masterTree = new Tree({
194+
value: 'Master',
195+
children: []
196+
});
197+
198+
spyOn(treeService.nodeExpanded$, 'next');
199+
spyOn(treeService.nodeCollapsed$, 'next');
200+
201+
treeService.fireNodeSwitchFoldingType(masterTree);
202+
203+
expect(treeService.nodeExpanded$.next).not.toHaveBeenCalled();
204+
expect(treeService.nodeCollapsed$.next).not.toHaveBeenCalled();
205+
});
206+
192207
it('fires "expanded" event for expanded tree', () => {
193208
const masterTree = new Tree({
194209
value: 'Master',

0 commit comments

Comments
 (0)