Skip to content

Commit 5ee70df

Browse files
jhonyeduardojhosefmarks
authored andcommitted
feat(tree-view): novo componente
Adiciona o novo componente Tree View ao pacote portinari-ui. Fixes DTHFUI-1740
1 parent a6ffb69 commit 5ee70df

26 files changed

+1150
-1
lines changed

projects/ui/src/lib/components/components.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { PoTableModule } from './po-table/po-table.module';
3232
import { PoTabsModule } from './po-tabs/po-tabs.module';
3333
import { PoTagModule } from './po-tag/po-tag.module';
3434
import { PoToolbarModule } from './po-toolbar/po-toolbar.module';
35+
import { PoTreeViewModule } from './po-tree-view/po-tree-view.module';
3536
import { PoWidgetModule } from './po-widget/po-widget.module';
3637

3738
@NgModule({
@@ -69,6 +70,7 @@ import { PoWidgetModule } from './po-widget/po-widget.module';
6970
PoTabsModule,
7071
PoTagModule,
7172
PoToolbarModule,
73+
PoTreeViewModule,
7274
PoWidgetModule
7375
],
7476
exports: [
@@ -104,6 +106,7 @@ import { PoWidgetModule } from './po-widget/po-widget.module';
104106
PoTabsModule,
105107
PoTagModule,
106108
PoToolbarModule,
109+
PoTreeViewModule,
107110
PoWidgetModule
108111
],
109112
providers: [],

projects/ui/src/lib/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export * from './po-table/index';
3232
export * from './po-tabs/index';
3333
export * from './po-tag/index';
3434
export * from './po-toolbar/index';
35+
export * from './po-tree-view/index';
3536
export * from './po-widget/index';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './po-tree-view-item/po-tree-view-item.interface';
2+
export * from './po-tree-view.component';
3+
4+
export * from './po-tree-view.module';
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { PoTreeViewBaseComponent } from './po-tree-view-base.component';
2+
3+
describe('PoTreeViewBaseComponent:', () => {
4+
5+
const component = new PoTreeViewBaseComponent();
6+
7+
it('should be created', () => {
8+
expect(component instanceof PoTreeViewBaseComponent).toBeTruthy();
9+
});
10+
11+
describe('Properties: ', () => {
12+
13+
it('p-items: shouldn`t call getItemsByMaxLevel if items isn`t array and return empty array', () => {
14+
const spyGetItemsByMaxLevel = spyOn(component, <any>'getItemsByMaxLevel');
15+
16+
component.items = undefined;
17+
18+
expect(spyGetItemsByMaxLevel).not.toHaveBeenCalled();
19+
expect(component.items).toEqual([]);
20+
});
21+
22+
it('p-items: should call getItemsByMaxLevel if items is array and return items', () => {
23+
const expectedValue = [{ label: 'Nível 01', value: 1 }];
24+
25+
const spyGetItemsByMaxLevel = spyOn(component, <any>'getItemsByMaxLevel').and.callThrough();
26+
27+
component.items = expectedValue;
28+
29+
expect(spyGetItemsByMaxLevel).toHaveBeenCalled();
30+
expect(component.items).toEqual(expectedValue);
31+
});
32+
});
33+
34+
describe('Methods: ', () => {
35+
36+
it('emitEvent: should call collapsed.emit with tree view item if treeViewItem.expanded is false', () => {
37+
const treeViewItem = { label: 'Nível 01', value: 1, expanded: false };
38+
39+
const spyCollapsedEmit = spyOn(component['collapsed'], 'emit');
40+
41+
component['emitEvent'](treeViewItem);
42+
43+
expect(spyCollapsedEmit).toHaveBeenCalledWith(treeViewItem);
44+
});
45+
46+
it('emitEvent: should call expanded.emit with tree view item if treeViewItem.expanded is true', () => {
47+
const treeViewItem = { label: 'Nível 01', value: 1, expanded: true };
48+
49+
const spyExpandedEmit = spyOn(component['expanded'], 'emit');
50+
51+
component['emitEvent'](treeViewItem);
52+
53+
expect(spyExpandedEmit).toHaveBeenCalledWith(treeViewItem);
54+
});
55+
56+
it('getItemsByMaxLevel: should return and not call addItem if level is 4', () => {
57+
const items = [];
58+
59+
const spyAddItem = spyOn(component, <any> 'addItem');
60+
61+
const itemsByMaxLavel = component['getItemsByMaxLevel'](items, 4);
62+
63+
expect(itemsByMaxLavel).toEqual(items);
64+
expect(spyAddItem).not.toHaveBeenCalled();
65+
});
66+
67+
it('getItemsByMaxLevel: should return `newItems` if `newItems` has value and `items` is equal `[]`', () => {
68+
const newItems = [{ item: 'first item' }];
69+
70+
const itemsByMaxLavel = component['getItemsByMaxLevel']([], undefined, undefined, newItems);
71+
72+
expect(itemsByMaxLavel).toEqual(newItems);
73+
});
74+
75+
it('getItemsByMaxLevel: should return `[]` if has no parameters', () => {
76+
const itemsByMaxLavel = component['getItemsByMaxLevel']();
77+
78+
expect(itemsByMaxLavel).toEqual([]);
79+
});
80+
81+
it('getItemsByMaxLevel: should return items up to 4 levels', () => {
82+
const unlimitedItems = [
83+
{ label: 'Nivel 01', value: 1, subItems: [
84+
{ label: 'Nivel 02', value: 2, subItems: [
85+
{ label: 'Nivel 03', value: 3, subItems: [
86+
{ label: 'Nivel 04', value: 4, subItems: [
87+
{ label: 'Nivel 05', value: 5, subItems: [
88+
{ label: 'Nivel 06', value: 6 }
89+
] }
90+
] }
91+
] }
92+
] }
93+
] }
94+
];
95+
96+
const expectedValue = [
97+
{ label: 'Nivel 01', value: 1, subItems: [
98+
{ label: 'Nivel 02', value: 2, subItems: [
99+
{ label: 'Nivel 03', value: 3, subItems: [
100+
{ label: 'Nivel 04', value: 4 }
101+
] }
102+
] }
103+
] }
104+
];
105+
106+
const spyAddItem = spyOn(component, <any> 'addItem').and.callThrough();
107+
const spyGetItemsByMaxLevel = spyOn(component, <any> 'getItemsByMaxLevel').and.callThrough();
108+
109+
const itemsByMaxLavel = component['getItemsByMaxLevel'](unlimitedItems);
110+
111+
expect(itemsByMaxLavel).toEqual(expectedValue);
112+
expect(spyAddItem).toHaveBeenCalled();
113+
expect(spyGetItemsByMaxLevel).toHaveBeenCalledTimes(5);
114+
});
115+
116+
it('addItem: should add childItem in items and not call expandParentItem and addChildItemInParent if parentIf is falsy', () => {
117+
const childItem = { label: 'Nível 01', value: 1 };
118+
const items = [];
119+
120+
const expectedValue = [childItem];
121+
122+
const spyExpandParentItem = spyOn(component, <any> 'expandParentItem');
123+
const spyAddChildItemInParent = spyOn(component, <any> 'addChildItemInParent');
124+
125+
component['addItem'](items, childItem);
126+
127+
expect(items.length).toBe(1);
128+
expect(items).toEqual(expectedValue);
129+
expect(spyAddChildItemInParent).not.toHaveBeenCalled();
130+
expect(spyExpandParentItem).not.toHaveBeenCalled();
131+
});
132+
133+
it('addItem: should add parentItem in items and call expandParentItem and addChildItemInParent', () => {
134+
const childItem = { label: 'Nível 02', value: 2 };
135+
const parentItem = { label: 'Nível 01', value: 1 };
136+
const items = [];
137+
138+
const expectedValue = [parentItem];
139+
140+
const spyExpandParentItem = spyOn(component, <any> 'expandParentItem');
141+
const spyAddChildItemInParent = spyOn(component, <any> 'addChildItemInParent');
142+
143+
component['addItem'](items, childItem, parentItem);
144+
145+
expect(items.length).toBe(1);
146+
expect(items).toEqual(expectedValue);
147+
expect(spyAddChildItemInParent).toHaveBeenCalledWith(childItem, parentItem);
148+
expect(spyExpandParentItem).toHaveBeenCalledWith(childItem, parentItem);
149+
});
150+
151+
it('addChildItemInParent: should create an empty array in parentItem.subItems if it is falsy and add childItem', () => {
152+
const childItem = { label: 'Nivel 02', value: 2 };
153+
const parentItem = { label: 'Nivel 01', value: 1, subItems: undefined };
154+
155+
component['addChildItemInParent'](childItem, parentItem);
156+
157+
expect(parentItem.subItems.length).toBe(1);
158+
expect(parentItem.subItems[0]).toEqual(childItem);
159+
});
160+
161+
it('addChildItemInParent: should add childItem in parentItem.subItems', () => {
162+
const childItem = { label: 'Nivel 02', value: 2 };
163+
const parentItem = { label: 'Nivel 01', value: 1, subItems: [{ label: 'Nivel 011', value: 111}] };
164+
165+
component['addChildItemInParent'](childItem, parentItem);
166+
167+
expect(parentItem.subItems.length).toBe(2);
168+
expect(parentItem.subItems[1]).toEqual(childItem);
169+
});
170+
171+
it('expandParentItem: parentItem.expanded should be true if childItem.expanded is true', () => {
172+
const childItem = { label: 'Nivel 2', value: 12, expanded: true };
173+
const parentItem = { label: 'Nivel 1', value: 1, expanded: undefined };
174+
175+
component['expandParentItem'](childItem, parentItem);
176+
177+
expect(parentItem.expanded).toBe(true);
178+
});
179+
180+
it('expandParentItem: parentItem.expanded should be true if childItem.expanded is false and parentItem.expanded is true', () => {
181+
const childItem = { label: 'Nivel 2', value: 12, expanded: false };
182+
const parentItem = { label: 'Nivel 1', value: 1, expanded: true };
183+
184+
component['expandParentItem'](childItem, parentItem);
185+
186+
expect(parentItem.expanded).toBe(true);
187+
});
188+
189+
it('expandParentItem: parentItem.expanded should be false if childItem.expanded is false', () => {
190+
const childItem = { label: 'Nivel 2', value: 12, expanded: false };
191+
const parentItem = { label: 'Nivel 1', value: 1, expanded: false };
192+
193+
component['expandParentItem'](childItem, parentItem);
194+
195+
expect(parentItem.expanded).toBe(false);
196+
});
197+
198+
});
199+
200+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { EventEmitter, Input, Output } from '@angular/core';
2+
3+
import { PoTreeViewItem } from './po-tree-view-item/po-tree-view-item.interface';
4+
5+
const poTreeViewMaxLevel = 4;
6+
7+
/**
8+
* @description
9+
*
10+
* O componente fornece um modelo de visualização em árvore, possibilitando a visualização das informações de maneira
11+
* hierárquica, desta forma sendo possível utilizar até 4 níveis.
12+
*
13+
* Nele é possível navegar entre os itens através da tecla *tab*, permitindo expandir ou colapsar o item em foco
14+
* por meio das teclas
15+
* *enter* e *space*.
16+
*/
17+
export class PoTreeViewBaseComponent {
18+
19+
private _items: Array<PoTreeViewItem> = [];
20+
21+
/**
22+
* Lista de itens do tipo `PoTreeViewItem` que será renderizada pelo componente.
23+
*/
24+
@Input('p-items') set items(value: Array<PoTreeViewItem>) {
25+
this._items = Array.isArray(value) ? this.getItemsByMaxLevel(value) : [];
26+
}
27+
28+
get items() {
29+
return this._items;
30+
}
31+
32+
/**
33+
* @optional
34+
*
35+
* @description
36+
*
37+
* Ação que será disparada ao expandir um item.
38+
*
39+
* > Como parâmetro o componente passará o objeto que originou o disparo.
40+
*/
41+
@Output('p-collapsed') collapsed = new EventEmitter<PoTreeViewItem>();
42+
43+
/**
44+
* @optional
45+
*
46+
* @description
47+
*
48+
* Ação que será disparada ao colapsar um item.
49+
*
50+
* > Como parâmetro o componente passará o objeto que originou o disparo.
51+
*/
52+
@Output('p-expanded') expanded = new EventEmitter<PoTreeViewItem>();
53+
54+
protected emitEvent(treeViewItem: PoTreeViewItem) {
55+
const event = treeViewItem.expanded ? 'expanded' : 'collapsed';
56+
57+
this[event].emit({ ...treeViewItem });
58+
}
59+
60+
private addChildItemInParent(childItem: PoTreeViewItem, parentItem: PoTreeViewItem) {
61+
if (!parentItem.subItems) {
62+
parentItem.subItems = [];
63+
}
64+
65+
parentItem.subItems.push(childItem);
66+
}
67+
68+
private addItem(items: Array<PoTreeViewItem>, childItem: PoTreeViewItem, parentItem?: PoTreeViewItem) {
69+
70+
if (parentItem) {
71+
this.expandParentItem(childItem, parentItem);
72+
this.addChildItemInParent(childItem, parentItem);
73+
74+
items.push(parentItem);
75+
} else {
76+
items.push(childItem);
77+
}
78+
79+
}
80+
81+
// expande o item pai caso o filho estiver expandido.
82+
private expandParentItem(childItem: PoTreeViewItem, parentItem: PoTreeViewItem) {
83+
if (childItem.expanded) {
84+
parentItem.expanded = true;
85+
}
86+
}
87+
88+
private getItemsByMaxLevel(items: Array<PoTreeViewItem> = [], level: number = 0, parentItem?: PoTreeViewItem, newItems = []) {
89+
items.forEach(item => {
90+
const { subItems, ...currentItem } = item;
91+
92+
if (level === poTreeViewMaxLevel) {
93+
return;
94+
}
95+
96+
if (Array.isArray(item.subItems)) {
97+
this.getItemsByMaxLevel(item.subItems, ++level, currentItem);
98+
--level;
99+
}
100+
101+
this.addItem(newItems, currentItem, parentItem);
102+
});
103+
104+
return newItems;
105+
}
106+
107+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="po-tree-view-item-header">
2+
3+
<button *ngIf="hasSubItems"
4+
class="po-tree-view-item-header-button"
5+
(click)="click.emit($event)">
6+
<span class="po-icon po-icon-arrow-down po-tree-view-item-header-button-icon"
7+
[class.po-tree-view-item-header-button-icon-transform]="expanded">
8+
</span>
9+
</button>
10+
11+
<span
12+
class="po-tree-view-item-header-label"
13+
[class.po-tree-view-item-header-label-padding]="!hasSubItems">
14+
{{ label }}
15+
</span>
16+
</div>

0 commit comments

Comments
 (0)