Skip to content

Commit

Permalink
feat(design)!: add tree renderMode (#2777)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Previously, the now-named renderMode "not-in-dom" was
the default. This is no longer the case. If one was rendering immensely
large trees and you now notice a significant slow-down, you can
change the renderMode like:

```ts
<ul daff-tree ... renderMode="not-in-dom">
 ...
</ul>
```

Which will restore the original behavior.
  • Loading branch information
damienwebdev committed Apr 27, 2024
1 parent 53b0d65 commit 2d8ec58
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('DaffioDocsPackagesListComponent', () => {

it('should render an anchor tag when the guide child has no children', () => {
const anchorTags = fixture.debugElement.queryAll(By.css('a'));
expect(anchorTags.length).toEqual(1);
expect(anchorTags.length).toEqual(2);
const buttons = fixture.debugElement.queryAll(By.css('button'));
expect(buttons.length).toEqual(1);
console.log(fixture.debugElement.nativeElement.innerHTML);
Expand Down
6 changes: 6 additions & 0 deletions libs/design/tree/src/interfaces/tree-render-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Represents the mode of rendering for nodes in a tree UI.
* - 'in-dom': Closed nodes are present in the Document Object Model (DOM).
* - 'not-in-dom': Closed nodes are not present in the Document Object Model (DOM).
*/
export type DaffTreeRenderMode = 'in-dom' | 'not-in-dom';
1 change: 1 addition & 0 deletions libs/design/tree/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { DaffTreeItemDirective } from './tree-item/tree-item.directive';
export { DaffTreeData } from './interfaces/tree-data';
export { DaffTreeUi } from './interfaces/tree-ui';
export { daffTransformTreeInPlace } from './utils/transform-in-place';
export { DaffTreeRenderMode } from './interfaces/tree-render-mode';
2 changes: 1 addition & 1 deletion libs/design/tree/src/tree/specs/defaults.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ describe('@daffodil/design/tree - DaffTreeComponent | Defaults', () => {

it('should have sane defaults', () => {
expect(component.flatTree).toEqual([]);
expect(component.dataTree).toEqual(undefined);
expect(component.tree).toEqual(undefined);
});
});
83 changes: 83 additions & 0 deletions libs/design/tree/src/tree/specs/render-modes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CommonModule } from '@angular/common';
import {
Component,
Input,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffTreeData } from '../../interfaces/tree-data';
import { DaffTreeRenderMode } from '../../interfaces/tree-render-mode';
import { DaffTreeModule } from '../../tree.module';
import { DaffTreeComponent } from '../tree.component';

@Component({
template: `
<ul daff-tree [tree]="data" [renderMode]="renderMode">
<ng-template #daffTreeItemWithChildrenTpl let-node>
<button daffTreeItem [node]="node">{{ node.title }} </button>
</ng-template>
<ng-template #daffTreeItemTpl let-node>
<a daffTreeItem [node]="node" href="node.url">{{ node.title }}</a>
</ng-template>
</ul>
`,
})
class WrapperComponent {
@Input() data: DaffTreeData<any>;
@Input() renderMode: DaffTreeRenderMode;
}


describe('@daffodil/design/tree - DaffTreeComponent | renderModes', () => {
let wrapper: WrapperComponent;
let component: DaffTreeComponent;
let fixture: ComponentFixture<WrapperComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DaffTreeModule, CommonModule],
declarations: [WrapperComponent],
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;
component = fixture.debugElement.query(By.css('ul[daff-tree]')).componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(wrapper).toBeTruthy();
});

it('should render two nodes when renderMode is `not-in-dom`', () => {
wrapper.data = { title: 'Root', url: '', id: '', items: [
{ title: 'Child A', url: '', id: '', items: [
{ title: 'Child Aa', url: '', id: '', items: [], data: {}},
], data: {}},
{ title: 'Child B', url: '', id: '', items: [], data: {}},
], data: {}};
wrapper.renderMode = 'not-in-dom';
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('li')).length).toEqual(2);
});

it('should render three nodes when renderMode is `in-dom`', () => {
wrapper.data = { title: 'Root', url: '', id: '', items: [
{ title: 'Child A', url: '', id: '', items: [
{ title: 'Child Aa', url: '', id: '', items: [], data: {}},
], data: {}},
{ title: 'Child B', url: '', id: '', items: [], data: {}},
], data: {}};
wrapper.renderMode = 'in-dom';
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('li')).length).toEqual(3);
});
});
2 changes: 1 addition & 1 deletion libs/design/tree/src/tree/specs/with-template.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ describe('@daffodil/design/tree - DaffTreeComponent | withTemplate', () => {
{ title: 'Child B', url: '', id: '', items: [], data: {}},
], data: {}};
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('li')).length).toEqual(2);
expect(fixture.debugElement.queryAll(By.css('li')).length).toEqual(3);
});
});
12 changes: 7 additions & 5 deletions libs/design/tree/src/tree/tree.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<ng-container *ngFor="let node of flatTree; trackBy: trackByTreeElement">
<li [attr.aria-level]="node.level">
<ng-container
*ngTemplateOutlet="node.hasChildren ? withChildrenTemplate : treeItemTemplate; context: { $implicit: node }">
</ng-container>
</li>
<ng-container>
<li [attr.aria-level]="node.level" [class.hidden]="!node.visible">
<ng-container
*ngTemplateOutlet="node.hasChildren ? withChildrenTemplate : treeItemTemplate; context: { $implicit: node }">
</ng-container>
</li>
</ng-container>
</ng-container>
4 changes: 4 additions & 0 deletions libs/design/tree/src/tree/tree.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
padding: 0;
list-style: none;
--tree-padding: 16px;

li.hidden {
display: none;
}
}

.daff-tree-item {
Expand Down
56 changes: 33 additions & 23 deletions libs/design/tree/src/tree/tree.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
ElementRef,
HostBinding,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChanges,
TemplateRef,
ViewEncapsulation,
} from '@angular/core';
Expand All @@ -15,6 +17,7 @@ import { daffArticleEncapsulatedMixin } from '@daffodil/design';

import { DaffTreeNotifierService } from './tree-notifier.service';
import { DaffTreeData } from '../interfaces/tree-data';
import { DaffTreeRenderMode } from '../interfaces/tree-render-mode';
import { DaffTreeUi } from '../interfaces/tree-ui';
import {
DaffTreeFlatNode,
Expand Down Expand Up @@ -62,7 +65,7 @@ const _daffTreeBase = daffArticleEncapsulatedMixin((DaffTreeBase));
DaffTreeNotifierService,
],
})
export class DaffTreeComponent extends _daffTreeBase implements OnInit {
export class DaffTreeComponent extends _daffTreeBase implements OnInit, OnChanges {

/**
* The css class of the daff-tree.
Expand All @@ -71,40 +74,32 @@ export class DaffTreeComponent extends _daffTreeBase implements OnInit {
*/
@HostBinding('class.daff-tree') class = true;

/**
* The rendering mode for nodes in the tree.
*
* Default value is 'in-dom', which means nodes are present in the DOM.
*
* Generally, `not-in-dom` is faster as there are less DOM elements to render,
* but there may be use-cases (like SEO) where having the tree in the DOM
* is relevant.
*/
@Input() renderMode: DaffTreeRenderMode;

/**
* The internal tree element.
*/
private tree: DaffTreeUi<unknown> = undefined;
private _tree: DaffTreeUi<unknown> = undefined;

/**
* The flattened tree data. You can iterate through this if you want to inspect
* the resulting array structure we computed to render the tree.
*/
public flatTree: DaffTreeFlatNode[] = [];

/**
* @docs-private
*/
private _dataTree: DaffTreeData<unknown> = undefined;

/**
* The tree data you would like to render.
*/
@Input('tree')
get dataTree() {
return this._dataTree;
}
set dataTree(dataTree: DaffTreeData<unknown>){
if(!dataTree) {
this._dataTree = undefined;
this.tree = undefined;
this.flatTree = [];
return;
}
this._dataTree = dataTree;
this.tree = hydrateTree(this.dataTree);
this.flatTree = flattenTree(this.tree);
};
@Input() tree: DaffTreeData<unknown>;

/**
* The template used to render tree-nodes that themselves have children.
Expand All @@ -129,6 +124,21 @@ export class DaffTreeComponent extends _daffTreeBase implements OnInit {
super(elementRef, renderer);
}

ngOnChanges(changes: SimpleChanges): void {
if(!changes.tree.currentValue) {
this._tree = undefined;
this.flatTree = [];
return;
}

if(changes.renderMode && !changes.tree) {
this.flatTree = flattenTree(this._tree, changes.renderMode.currentValue === 'not-in-dom');
} else if(changes.renderMode || changes.tree) {
this._tree = hydrateTree(changes.tree?.currentValue ?? this.tree);
this.flatTree = flattenTree(this._tree, (changes.renderMode?.currentValue ?? this.renderMode) === 'not-in-dom');
}
}

/**
* The track-by function used to reduce tree-item re-renders
*/
Expand All @@ -141,7 +151,7 @@ export class DaffTreeComponent extends _daffTreeBase implements OnInit {
*/
ngOnInit(): void {
this.notifier.notice$.subscribe(() => {
this.flatTree = flattenTree(this.tree);
this.flatTree = flattenTree(this._tree, this.renderMode === 'not-in-dom');
});
}
}
49 changes: 42 additions & 7 deletions libs/design/tree/src/utils/flatten-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { flattenTree } from './flatten-tree';
import { hydrateTree } from './hydrate-tree';
import { traverse } from './traverse-tree';
import { DaffTreeUi } from '../interfaces/tree-ui';

describe('@daffodil/design/tree - flattenTree', () => {
it('should flatten a root into an empty array', () => {
it('should flatten a root into an empty array ', () => {
const data = { title: '', url: '', id: '', items: [], data: {}};
const flat = [];

expect(flattenTree(hydrateTree(data))).toEqual(flat);
});

it('should flatten a root into an empty array when removeNodes is true', () => {
const data = { title: '', url: '', id: '', items: [], data: {}};
const flat = [];

expect(flattenTree(hydrateTree(data), true)).toEqual(flat);
});

it('should flatten a data tree into a tree with an open first layer and closed lower layers', () => {
const data = { title: 'Root', url: '', id: '', items: [
{ title: 'Child A', url: '', id: '', items: [
Expand All @@ -24,7 +30,14 @@ describe('@daffodil/design/tree - flattenTree', () => {
const flat = flattenTree(hydrateTree(data));

expect(flat[0].title).toEqual('Child A');
expect(flat[1].title).toEqual('Child B');
expect(flat[0].visible).toEqual(true);
expect(flat[1].title).toEqual('Child Aa');
expect(flat[1].visible).toEqual(false);

const flatRemoved = flattenTree(hydrateTree(data), true);

expect(flatRemoved[0].title).toEqual('Child A');
expect(flatRemoved[1].title).toEqual('Child B');
});

it('should flatten an open ui tree', () => {
Expand Down Expand Up @@ -84,13 +97,24 @@ describe('@daffodil/design/tree - flattenTree', () => {
childB.items = [childBb];
childBb.parent = childB;


const flat = flattenTree(root);

expect(flat[0].title).toEqual('Child A');
expect(flat[0].visible).toEqual(true);
expect(flat[1].title).toEqual('Child Aa');
expect(flat[1].visible).toEqual(true);
expect(flat[2].title).toEqual('Child B');
expect(flat[2].visible).toEqual(true);
expect(flat[3].title).toEqual('Child Bb');
expect(flat[3].visible).toEqual(true);


const flatRemoved = flattenTree(root, true);

expect(flatRemoved[0].title).toEqual('Child A');
expect(flatRemoved[1].title).toEqual('Child Aa');
expect(flatRemoved[2].title).toEqual('Child B');
expect(flatRemoved[3].title).toEqual('Child Bb');
});

it('should clip closed branches', () => {
Expand Down Expand Up @@ -151,11 +175,22 @@ describe('@daffodil/design/tree - flattenTree', () => {
childBb.parent = childB;


const flatRemoved = flattenTree(root, true);

expect(flatRemoved[0].title).toEqual('Child A');
expect(flatRemoved[1].title).toEqual('Child B');
expect(flatRemoved[2].title).toEqual('Child Bb');

const flat = flattenTree(root);

expect(flat[0].title).toEqual('Child A');
expect(flat[1].title).toEqual('Child B');
expect(flat[2].title).toEqual('Child Bb');
expect(flat[0].visible).toEqual(true);
expect(flat[1].title).toEqual('Child Aa');
expect(flat[1].visible).toEqual(false);
expect(flat[2].title).toEqual('Child B');
expect(flat[2].visible).toEqual(true);
expect(flat[3].title).toEqual('Child Bb');
expect(flat[3].visible).toEqual(true);
});

it('should handle deep trees correctly', () => {
Expand Down Expand Up @@ -217,7 +252,7 @@ describe('@daffodil/design/tree - flattenTree', () => {
childAaAa.parent = childAaA;


const flat = flattenTree(root);
const flat = flattenTree(root, true);

expect(flat[0].title).toEqual('Child A');
expect(flat[1].title).toEqual('Child Aa');
Expand Down
Loading

0 comments on commit 2d8ec58

Please sign in to comment.