3
votes

I have a page with PrimeNG tree and an autocomplete field. My requirement is Tree should be expanded to the matching node when user enters and selects a text in autocomplete field and also it should scroll to the matching node and highlight the node.

I tried to expand the tree by setting 'expanded' property to 'true'. But I am not finding a way to scroll to selected node. Any help on this is appreciated.

Also please let me know if there is any method that expands the tree using selected node.

4

4 Answers

2
votes

Probably not the most beautiful solution, but you can achieve this by using the following util method.

 public scrollToSelectionPrimeNgDataTable(table: DataTable, element: HTMLElement) {
    if (table.selection !== null && table.value !== null) {
        let index = table.value.indexOf(table.selection);
        let list = document.querySelectorAll('tr');
        if (list !== null && index < list.length) {
            let targetElement = list.item(index);
            targetElement.scrollIntoView()
        }
    }
}

To use this Method you have to pass a reference of the DataTable and the table itself as HTMLElement to the method. You can get both by using Angular2's @ViewChild decorator.

2
votes

Expanding on David Asher's answer, this is a simpler working solution for PrimeNG's tree:

HTML:

<p-tree #mytreeid id="mytree"></p-tree>

Angular:

@ViewChild("mytree") mytree: Tree;

// selection is the TreeNode you want to scroll into view
scrollToSelectionPrimeNgDataTree(selection, tree, elementIdName) {
      if (tree.value !== null) {
          let index = tree.value.indexOf(selection);
          document.getElementById(elementIdName).querySelectorAll("p-treenode")[index].scrollIntoView();
      }
  }
0
votes

Adding on John's and David's answers. Following statement returns all nodes in tree (including children) as a flat array of Elements.

document.getElementById(elementIdName).querySelectorAll("p-treenode")

Therefore, the index of the searched node should be calculated correctly. It depends on whether node is expanded or not. Node tree should be traversed in depth-first (pre-order) manner to find node's index. It can be implemented using recursion:

private CalculateIndex(tree: TreeNode[], predicate: (node: TreeNode) => boolean,
  startIndex: number = 0 ): { found: boolean; index: number } 
 {
  let index: number = startIndex;
  let found = false;
  for (const node of tree) {
     found = predicate(node);
     if (found) {
        break;
     }

     index++;
     if (node.expanded) {
        ({ found, index } = this.CalculateIndex(node.children, predicate, index));
        if (found) {
           break;
        }
     }
  }

  return { found, index };

}

Found index then can be used in the following function to scroll to the node:

private scrollToSelection(selection: TreeNode, treeNodes: TreeNode[], treeElementId: string) {
  const { found, index } = this.CalculateIndex(treeNodes, (node: TreeNode) => node === selection);
  if (found) {
     const treeElement = document.getElementById(treeElementId);
     const allTreeNodes = treeElement.querySelectorAll('p-treenode');
     const node = allTreeNodes[index];
     if (node) {
        node.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'center'});
     }
  }

}

Hope it helped a little.

0
votes

These all have assumptions. John's seems to assume a root item. Vadym's assumes tree is fully expanded. I use mine as the form is initially loaded. I expand the selected item to the root array and leave the remaining nested tree elements collapsed. Additionally, I have populated the parent. I have created the following static helper method version of John's solution:

/*
** Scroll to selected item.
**
** Tree value is obtained via:
**  <p-tree #MyTree [value]='data' name='myTree' selectionMode='single' [(selection)]='selected'></p-tree>
**  @ViewChild( 'MyTree' ) mtree: Tree;
** elementIdName value is in above example: myTree
** Example:
**  _PCOMMON.treeScrollToSelectedItem( mtree, 'myTree' );
*/
public static treeScrollToSelectedItem( tree: Tree, elementIdName: string ): number {
    let idx: number = -1;
    if ( tree.value !== null && tree.selection !== undefined ) {
        idx = 0; // Count levels deep
        let items: TreeNode[] = [];
        let node: TreeNode = tree.selection;
        do {
            if( node.parent !== undefined && node.parent !== null ) {
                items = node.parent.children; // begin of level
                idx++;
            } else {
                items = tree.value;
            }
            idx = items.findIndex( k => k.key === node.key ) + idx;
            node = node.parent;
        }
        while ( node !== undefined && node !== null );
        if( idx > -1 ) {
            document.getElementById( elementIdName ).querySelectorAll( 'p-treenode' )[ idx ].scrollIntoView( );
        }
    }
    return idx;
}

This is the testing structure:

import { Component, ElementRef, ViewChild, OnInit } from '@angular/core';
import { TreeNode, SelectItem } from 'primeng/api';
import { TreeModule, Tree } from 'primeng/tree';
import { _PCOMMON } from './pcommon';
//
@Component({
    template: `<p-tree #MyTree [value]='treeData' id='myTree' name='myTree' selectionMode='single' [(selection)]='selected'></p-tree>`
})
class TestTreeComponent implements OnInit {
    @ViewChild( 'MyTree' ) mtree: Tree;
    treeData: TreeNode[];
    selected: TreeNode = undefined;
    ngOnInit () {
        this.treeData = [
            { label: 'One (1)', key: '1', children: [], parent: undefined, expanded: false },
            { label: 'Two (2)', key: '2', children: [], parent: undefined, expanded: false },
            { label: 'Three (3)', key: '3', children: [], parent: undefined, expanded: true },
            { label: 'Four (4)', key: '4', children: [], parent: undefined, expanded: false }
        ];
        const children: TreeNode[] = [
            { label: 'Five (5)', key: '5', children: [], parent: this.treeData[2], expanded: false },
            { label: 'Six (6)', key: '6', children: [], parent: this.treeData[2], expanded: false },
            { label: 'Seven (7)', key: '7', children: [], parent: this.treeData[2], expanded: true }
        ];
        const children2: TreeNode[] = [
            { label: 'Eight (8)', key: '8', children: [], parent: children[2], expanded: false },
            { label: 'Nine (9)', key: '9', children: [], parent: children[2], expanded: false }
        ];
        this.treeData[2].children = children;
        this.treeData[2].children[2].children = children2;
    }
    //
}
//
import { TestBed, waitForAsync, ComponentFixture, inject, fakeAsync, tick } from '@angular/core/testing';
//
fdescribe('_PCOMMON_TreeComponent', () => {
    /*
    ** The treeData is as follows:
    ** One (1)
    ** Two (2)
    ** Three (3)
    **   Five (5)
    **   Six (6)
    **   Seven (7)
    **     Eight (8)
    **     Nine (9)
    ** Four (4)
    */
    let sut: TestTreeComponent;
    let fixture: ComponentFixture<TestTreeComponent>;
    //
    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [ TreeModule ],
            declarations: [ Tree, TestTreeComponent ]
        });
        //
        fixture = TestBed.createComponent( TestTreeComponent );
        sut = fixture.componentInstance;
        //
        fixture.detectChanges( ); // trigger initial data binding
        fixture.whenStable( );
    });
    /*
    ** public static treeScrollToSelectedItem( tree: Tree, elementIdName: string ): number
    */
    it( 'treeScrollToSelectedItem, nested selected ...', fakeAsync( ( ) => {
        // given
        sut.selected = sut.treeData[2].children[2].children[1];
        tick( 100 );
        fixture.detectChanges( ); // trigger initial data binding
        fixture.whenStable( );
        console.warn( sut.mtree.selection );
        // when
        const ret: number = _PCOMMON.treeScrollToSelectedItem( sut.mtree, 'myTree' );
        // then
        expect( ret ).toEqual( 7 );
    } ) );
    //
    it( 'treeScrollToSelectedItem, first nested selected ...', fakeAsync( ( ) => {
        // given
        sut.selected = sut.treeData[2].children[0];
        tick( 100 );
        fixture.detectChanges( ); // trigger initial data binding
        fixture.whenStable( );
        // when
        const ret: number = _PCOMMON.treeScrollToSelectedItem( sut.mtree, 'myTree' );
        // then
        console.warn( ret );
        expect( ret ).toEqual( 3 );
    } ) );
    //
    it( 'treeScrollToSelectedItem, root item ...', fakeAsync( ( ) => {
        // given
        sut.selected = sut.treeData[2];
        tick( 100 );
        fixture.detectChanges( ); // trigger initial data binding
        fixture.whenStable( );
        // when
        const ret: number = _PCOMMON.treeScrollToSelectedItem( sut.mtree, 'myTree' );
        // then
        expect( ret ).toEqual( 2 );
    } ) );
    //
});