Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tree-grid): allow specify getters for node properties #1254

Merged
merged 7 commits into from
Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,6 @@ export const structure = [
icon: 'tree-grid.svg',
source: [
'NbTreeGridComponent',
'NbTreeGridNode',
'NbTreeGridPresentationNode',
'NbTreeGridSortService',
'NbTreeGridFilterService',
Expand Down
6 changes: 6 additions & 0 deletions src/app/playground-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,12 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [
component: 'TreeGridDisableClickToggleComponent',
name: 'Tree Grid Disable Click Toggle',
},
{
path: 'tree-grid-custom-node-structure.component',
link: '/tree-grid/tree-grid-custom-node-structure.component',
component: 'TreeGridCustomNodeStructureComponent',
name: 'Tree Grid Custom Node Structure',
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { NbDataSource } from '../../cdk/table';
import { NbCollectionViewer } from '../../cdk/collections';
import { NbTreeGridSortService } from './tree-grid-sort.service';
import { NbDataSource } from '../../cdk/table';
import { NbSortable, NbSortRequest } from '../tree-grid-sort.component';
import { NbTreeGridDataService } from './tree-grid-data.service';
import { NbTreeGridFilterService } from './tree-grid-filter.service';
import { NbTreeGridSortService } from './tree-grid-sort.service';
import {
ChildrenGetter,
DataGetter,
DEFAULT_ROW_LEVEL, ExpandedGetter,
NbTreeGridPresentationNode,
} from './tree-grid.model';
import { NbToggleOptions, NbTreeGridService } from './tree-grid.service';
import { NbTreeGridDataService } from './tree-grid-data.service';
import { NbSortable, NbSortRequest } from '../tree-grid-sort.component';
import { DEFAULT_ROW_LEVEL, NbTreeGridNode, NbTreeGridPresentationNode } from './tree-grid.model';

export interface NbFilterable {
filter(filterRequest: string);
Expand All @@ -40,9 +45,14 @@ export class NbTreeGridDataSource<T> extends NbDataSource<NbTreeGridPresentation
super();
}

setData(data: NbTreeGridNode<T>[]) {
setData<N>(
data: N[],
dataGetter?: DataGetter<N, T>,
childrenGetter?: ChildrenGetter<N, T>,
expandedGetter?: ExpandedGetter<T>,
) {
const presentationData: NbTreeGridPresentationNode<T>[] = data
? this.treeGridDataService.toPresentationNodes(data)
? this.treeGridDataService.toPresentationNodes(data, dataGetter, childrenGetter, expandedGetter)
: [];
this.data = new BehaviorSubject(presentationData);
this.updateChangeSubscription();
Expand Down Expand Up @@ -130,15 +140,20 @@ export class NbTreeGridDataSourceBuilder<T> {
private treeGridDataService: NbTreeGridDataService<T>) {
}

create(data: NbTreeGridNode<T>[]): NbTreeGridDataSource<T> {
create<N>(
data: N[],
dataGetter?: DataGetter<N, T>,
yggg marked this conversation as resolved.
Show resolved Hide resolved
childrenGetter?: ChildrenGetter<N, T>,
expandedGetter?: ExpandedGetter<T>,
): NbTreeGridDataSource<T> {
const dataSource = new NbTreeGridDataSource<T>(
this.sortService,
this.filterService,
this.treeGridService,
this.treeGridDataService,
);

dataSource.setData(data);
dataSource.setData(data, dataGetter, childrenGetter, expandedGetter);
return dataSource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,34 @@
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { Injectable } from '@angular/core';

import { DEFAULT_ROW_LEVEL, NbTreeGridNode, NbTreeGridPresentationNode } from './tree-grid.model';
import {
ChildrenGetter,
DataGetter,
DEFAULT_ROW_LEVEL,
ExpandedGetter,
NbTreeGridPresentationNode,
} from './tree-grid.model';

@Injectable()
export class NbTreeGridDataService<T> {

toPresentationNodes(nodes: NbTreeGridNode<T>[], level: number = DEFAULT_ROW_LEVEL): NbTreeGridPresentationNode<T>[] {
return nodes.map((node: NbTreeGridNode<T>) => {
const presentationNode = new NbTreeGridPresentationNode(node, level);

if (node.children) {
presentationNode.children = this.toPresentationNodes(node.children, level + 1);
}

return presentationNode;
toPresentationNodes<N>(
nodes: N[],
dataGetter: DataGetter<N, T> = node => node.data,
yggg marked this conversation as resolved.
Show resolved Hide resolved
childrenGetter: ChildrenGetter<N, T> = d => d.children || undefined,
expandedGetter: ExpandedGetter<T> = d => d.expanded,
level: number = DEFAULT_ROW_LEVEL,
): NbTreeGridPresentationNode<T>[] {
return nodes.map(node => {
const children = childrenGetter(node);
const presentationChildren: NbTreeGridPresentationNode<T>[] = children
yggg marked this conversation as resolved.
Show resolved Hide resolved
? this.toPresentationNodes(children, dataGetter, childrenGetter, expandedGetter, level + 1)
: undefined;

return new NbTreeGridPresentationNode(dataGetter(node), presentationChildren, expandedGetter(node), level);
});
}

Expand All @@ -36,14 +48,11 @@ export class NbTreeGridDataService<T> {

copy(nodes: NbTreeGridPresentationNode<T>[]): NbTreeGridPresentationNode<T>[] {
return nodes.map((node: NbTreeGridPresentationNode<T>) => {
const presentationNode = new NbTreeGridPresentationNode(node.node, node.level);
presentationNode.expanded = node.expanded;

if (node.hasChildren()) {
presentationNode.children = this.copy(node.children);
}
const children: NbTreeGridPresentationNode<T>[] = node.hasChildren()
yggg marked this conversation as resolved.
Show resolved Hide resolved
? this.copy(node.children)
: undefined;

return presentationNode;
return new NbTreeGridPresentationNode(node.data, children, node.expanded, node.level);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class NbTreeGridFilterService<T> {
if (filteredChildren && filteredChildren.length) {
node.expanded = true;
filtered.push(node);
} else if (this.filterPredicate(node.node.data, query)) {
} else if (this.filterPredicate(node.data, query)) {
filtered.push(node);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,27 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

/**
* Table's data interface
*/
export interface NbTreeGridNode<T> {
/**
* Data object which will be available as a context of rows and cell templates
* @type T
*/
data: T,
/**
* Child rows
*/
children?: NbTreeGridNode<T>[];
/**
* Row expand state
*/
expanded?: boolean;
}

export const DEFAULT_ROW_LEVEL: number = 0;
yggg marked this conversation as resolved.
Show resolved Hide resolved

export type DataGetter<N, T> = (N) => T;
export type ChildrenGetter<N, T> = (N) => (T[] | undefined);
export type ExpandedGetter<T> = (T) => boolean;

/**
* Implicit context of cells and rows
*/
export class NbTreeGridPresentationNode<T> {
/**
* Row expand state
*/
get expanded(): boolean {
return this.node.expanded;
}
set expanded(value: boolean) {
this.node.expanded = value;
}
children: NbTreeGridPresentationNode<T>[] = [];

/**
* Data object associated with row
*/
get data(): T {
return this.node.data;
}

constructor(
readonly node: NbTreeGridNode<T>,
public readonly level: number = DEFAULT_ROW_LEVEL,
/**
* Data object associated with row
*/
public readonly data: T,
public children: NbTreeGridPresentationNode<T>[] | undefined,
/**
* Row expand state
*/
public expanded: boolean,
public readonly level: number,
) {}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class NbTreeGridService<T> {
const toCheck: NbTreeGridPresentationNode<T>[] = [...data];

for (const node of toCheck) {
if (node.node.data === row) {
if (node.data === row) {
return node;
}

Expand Down
103 changes: 96 additions & 7 deletions src/framework/theme/components/tree-grid/tree-grid.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { Component, QueryList, Type, ViewChild, ViewChildren } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { EMPTY } from 'rxjs';
import { take } from 'rxjs/operators';
import createSpy = jasmine.createSpy;
import {
NbThemeModule,
NbTreeGridComponent,
NbTreeGridDataSource,
NbTreeGridModule,
NbTreeGridNode,
NbTreeGridRowComponent,
NB_ROW_DOUBLE_CLICK_DELAY,
NbTreeGridDataSourceBuilder,
NbTreeGridPresentationNode,
DataGetter,
ChildrenGetter,
ExpandedGetter,
} from '@nebular/theme';

interface TreeNode<T> {
data: T;
expanded?: boolean,
children?: TreeNode<T>[];
}

interface CustomStructure {
a: string;
b: string;
c: string;
childNodes?: CustomStructure[];
expanded?: boolean;
}

class BaseTreeGridTestComponent {
columns: string[];
dataSource: NbTreeGridDataSource<any>;
Expand Down Expand Up @@ -50,7 +71,7 @@ export class TreeGridWithHeaderTestComponent extends BaseTreeGridTestComponent {
function setupFixture(
componentType: Type<any>,
columns: string[],
data?: NbTreeGridNode<any>[],
data?: TreeNode<any>[],
): ComponentFixture<any> {

TestBed.configureTestingModule({
Expand All @@ -69,24 +90,29 @@ function setupFixture(
}

const abcColumns: string[] = [ 'a', 'b', 'c' ];
const twoRowsData: NbTreeGridNode<any>[] = [
const twoRowsData: TreeNode<any>[] = [
{ data: { a: 'a1', b: 'b1', c: 'c1' } },
{ data: { a: 'a2', b: 'b2', c: 'c2' } },
];
const nestedRowData: NbTreeGridNode<any>[] = [
const nestedRowData: TreeNode<any>[] = [
{
data: { a: 'a1', b: 'b1', c: 'c1' },
children: [ { data: { a: 'a2', b: 'b2', c: 'c2' } } ],
},
];
const nestedExpandedRowData: NbTreeGridNode<any>[] = [
const nestedExpandedRowData: TreeNode<any>[] = [
{
data: { a: 'a1', b: 'b1', c: 'c1' },
expanded: true,
children: [ { data: { a: 'a2', b: 'b2', c: 'c2' } } ],
},
];

const customStructureData: CustomStructure[] = [
{
a: 'a1', b: 'b1', c: 'c1', expanded: true,
childNodes: [{ a: 'a2', b: 'b2', c: 'c2' }],
},
];

describe('NbTreeGridComponent', () => {

Expand Down Expand Up @@ -177,4 +203,67 @@ describe('NbTreeGridComponent', () => {
const rows = fixture.nativeElement.querySelectorAll('.nb-tree-grid-row');
expect(rows.length).toEqual(2);
}));

describe('NbTreeGridDataSourceBuilder custom node getters', () => {
const mockConnectionViewer = { viewChange: EMPTY };
const dataGetter: DataGetter<CustomStructure, CustomStructure> = node => node;
const childrenGetter: ChildrenGetter<CustomStructure, CustomStructure> = node => node.childNodes;
const expandedGetter: ExpandedGetter<CustomStructure> = node => !!node.expanded;
let dataSourceBuilder: NbTreeGridDataSourceBuilder<CustomStructure>;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ NbThemeModule.forRoot(), NbTreeGridModule ],
});
});
beforeEach(inject([ NbTreeGridDataSourceBuilder ], builder => {
dataSourceBuilder = builder;
}));

it('should use custom data accessor if provided', fakeAsync(() => {
const dataGetterSpy = createSpy('dataGetter', dataGetter).and.callThrough();

const dataSource = dataSourceBuilder.create(customStructureData, dataGetterSpy, childrenGetter, expandedGetter);
expect(dataGetterSpy).toHaveBeenCalledTimes(2);

let presentationNodes: NbTreeGridPresentationNode<CustomStructure>[] = [];
dataSource.connect(mockConnectionViewer)
.pipe(take(1))
.subscribe(nodes => presentationNodes = nodes as NbTreeGridPresentationNode<CustomStructure>[]);
tick();

expect(presentationNodes[0].data).toEqual(customStructureData[0]);
expect(presentationNodes[1].data).toEqual(customStructureData[0].childNodes[0]);
}));

it('should use custom children accessor if provided', fakeAsync(() => {
const childrenGetterSpy = createSpy('childrenGetter', childrenGetter).and.callThrough();

const dataSource = dataSourceBuilder.create(customStructureData, dataGetter, childrenGetterSpy, expandedGetter);
expect(childrenGetterSpy).toHaveBeenCalledTimes(2);

let presentationNodes: NbTreeGridPresentationNode<CustomStructure>[] = [];
dataSource.connect(mockConnectionViewer)
.pipe(take(1))
.subscribe(nodes => presentationNodes = nodes as NbTreeGridPresentationNode<CustomStructure>[]);
tick();

expect(presentationNodes[0].data.childNodes[0]).toEqual(customStructureData[0].childNodes[0]);
}));

it('should use custom expanded accessor if provided', fakeAsync(() => {
const expandedGetterSpy = createSpy('expandedGetter', expandedGetter).and.callThrough();

const dataSource = dataSourceBuilder.create(customStructureData, dataGetter, childrenGetter, expandedGetterSpy);
expect(expandedGetterSpy).toHaveBeenCalledTimes(2);

let presentationNodes: NbTreeGridPresentationNode<CustomStructure>[] = [];
dataSource.connect(mockConnectionViewer)
.pipe(take(1))
.subscribe(nodes => presentationNodes = nodes as NbTreeGridPresentationNode<CustomStructure>[]);
tick();

expect(presentationNodes[0].expanded).toEqual(true);
}));
});
});
Loading