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,