diff --git a/apps/daffio/src/app/packages/components/packages-list/packages-list.component.spec.ts b/apps/daffio/src/app/packages/components/packages-list/packages-list.component.spec.ts
index 1c61109f53..1ecb0d6c6f 100644
--- a/apps/daffio/src/app/packages/components/packages-list/packages-list.component.spec.ts
+++ b/apps/daffio/src/app/packages/components/packages-list/packages-list.component.spec.ts
@@ -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);
diff --git a/libs/design/tree/src/interfaces/tree-render-mode.ts b/libs/design/tree/src/interfaces/tree-render-mode.ts
new file mode 100644
index 0000000000..a024b4cc46
--- /dev/null
+++ b/libs/design/tree/src/interfaces/tree-render-mode.ts
@@ -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';
diff --git a/libs/design/tree/src/public_api.ts b/libs/design/tree/src/public_api.ts
index 7bbafa2b13..de42b2cfcc 100644
--- a/libs/design/tree/src/public_api.ts
+++ b/libs/design/tree/src/public_api.ts
@@ -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';
diff --git a/libs/design/tree/src/tree/specs/defaults.spec.ts b/libs/design/tree/src/tree/specs/defaults.spec.ts
index 0c7acfc37b..b3711982b2 100644
--- a/libs/design/tree/src/tree/specs/defaults.spec.ts
+++ b/libs/design/tree/src/tree/specs/defaults.spec.ts
@@ -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);
});
});
diff --git a/libs/design/tree/src/tree/specs/render-modes.spec.ts b/libs/design/tree/src/tree/specs/render-modes.spec.ts
new file mode 100644
index 0000000000..b6321f0b55
--- /dev/null
+++ b/libs/design/tree/src/tree/specs/render-modes.spec.ts
@@ -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: `
+
+ `,
+})
+class WrapperComponent {
+ @Input() data: DaffTreeData;
+ @Input() renderMode: DaffTreeRenderMode;
+}
+
+
+describe('@daffodil/design/tree - DaffTreeComponent | renderModes', () => {
+ let wrapper: WrapperComponent;
+ let component: DaffTreeComponent;
+ let fixture: ComponentFixture;
+
+ 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);
+ });
+});
diff --git a/libs/design/tree/src/tree/specs/with-template.spec.ts b/libs/design/tree/src/tree/specs/with-template.spec.ts
index 5f4fc72c97..9d726a617e 100644
--- a/libs/design/tree/src/tree/specs/with-template.spec.ts
+++ b/libs/design/tree/src/tree/specs/with-template.spec.ts
@@ -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);
});
});
diff --git a/libs/design/tree/src/tree/tree.component.html b/libs/design/tree/src/tree/tree.component.html
index 9feaee3429..6acacb3481 100644
--- a/libs/design/tree/src/tree/tree.component.html
+++ b/libs/design/tree/src/tree/tree.component.html
@@ -1,7 +1,9 @@
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/design/tree/src/tree/tree.component.scss b/libs/design/tree/src/tree/tree.component.scss
index 608e79fe44..4210f023a9 100644
--- a/libs/design/tree/src/tree/tree.component.scss
+++ b/libs/design/tree/src/tree/tree.component.scss
@@ -10,6 +10,10 @@
padding: 0;
list-style: none;
--tree-padding: 16px;
+
+ li.hidden {
+ display: none;
+ }
}
.daff-tree-item {
diff --git a/libs/design/tree/src/tree/tree.component.ts b/libs/design/tree/src/tree/tree.component.ts
index aaf002c4f9..850697140e 100644
--- a/libs/design/tree/src/tree/tree.component.ts
+++ b/libs/design/tree/src/tree/tree.component.ts
@@ -5,8 +5,10 @@ import {
ElementRef,
HostBinding,
Input,
+ OnChanges,
OnInit,
Renderer2,
+ SimpleChanges,
TemplateRef,
ViewEncapsulation,
} from '@angular/core';
@@ -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,
@@ -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.
@@ -71,10 +74,21 @@ 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 = undefined;
+ private _tree: DaffTreeUi = undefined;
/**
* The flattened tree data. You can iterate through this if you want to inspect
@@ -82,29 +96,10 @@ export class DaffTreeComponent extends _daffTreeBase implements OnInit {
*/
public flatTree: DaffTreeFlatNode[] = [];
- /**
- * @docs-private
- */
- private _dataTree: DaffTreeData = undefined;
-
/**
* The tree data you would like to render.
*/
- @Input('tree')
- get dataTree() {
- return this._dataTree;
- }
- set dataTree(dataTree: DaffTreeData){
- 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;
/**
* The template used to render tree-nodes that themselves have children.
@@ -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
*/
@@ -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');
});
}
}
diff --git a/libs/design/tree/src/utils/flatten-tree.spec.ts b/libs/design/tree/src/utils/flatten-tree.spec.ts
index 46dd61180a..c6fb6d44b4 100644
--- a/libs/design/tree/src/utils/flatten-tree.spec.ts
+++ b/libs/design/tree/src/utils/flatten-tree.spec.ts
@@ -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: [
@@ -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', () => {
@@ -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', () => {
@@ -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', () => {
@@ -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');
diff --git a/libs/design/tree/src/utils/flatten-tree.ts b/libs/design/tree/src/utils/flatten-tree.ts
index 1c7c36a7ed..bcf61bb424 100644
--- a/libs/design/tree/src/utils/flatten-tree.ts
+++ b/libs/design/tree/src/utils/flatten-tree.ts
@@ -11,6 +11,7 @@ export interface DaffTreeFlatNode {
level: number;
hasChildren: boolean;
data: unknown;
+ visible: boolean;
_treeRef: DaffTreeUi;
}
@@ -18,15 +19,16 @@ export interface DaffTreeFlatNode {
* Flatten a DaffTreeUi into an array, removing elements from the array
* below nodes in the tree that are not open.
*/
-export const flattenTree = (daffUiTree: DaffTreeUi): DaffTreeFlatNode[] => {
+export const flattenTree = (daffUiTree: DaffTreeUi, removeNodes: boolean = false): DaffTreeFlatNode[] => {
const tree: DaffTreeFlatNode[] = [];
+ if(!daffUiTree) {
+ return [];
+ }
let items = [
{
...daffUiTree,
- title: 'Root',
level: 0,
- url: '/',
data: undefined,
open: true,
_treeRef: daffUiTree,
@@ -40,24 +42,34 @@ export const flattenTree = (daffUiTree: DaffTreeUi): DaffTreeFlatNode[]
break;
}
- if(el.open) {
- items = [
- ...items,
- ...el.items.map((i) => ({
- ...i,
- level:
- el.level + 1,
- _treeRef: i,
- })).reverse(),
- ];
- }
+ items = [
+ ...items,
+ ...el.items.map((i) => ({
+ ...i,
+ level:
+ el.level + 1,
+ _treeRef: i,
+ })).reverse(),
+ ];
- if(el._treeRef.parent?.open) {
+ if(!removeNodes && el._treeRef.parent) {
+ tree.push({
+ id: el.id,
+ title: el.title,
+ level: el.level,
+ url : el.url,
+ visible: el._treeRef.parent?.open,
+ hasChildren: el.items.length > 0,
+ data: undefined,
+ _treeRef: el._treeRef,
+ });
+ } else if(removeNodes && el._treeRef.parent?.open) {
tree.push({
id: el.id,
title: el.title,
level: el.level,
url : el.url,
+ visible: el._treeRef.parent?.open,
hasChildren: el.items.length > 0,
data: undefined,
_treeRef: el._treeRef,