New to Telerik UI for Blazor? Download free 30-day trial

Scroll to TreeView Item

Environment

Product TreeView for Blazor

Description

I expand a TreeView parent item and select a child item. The child item may be outside the visible part of the page viewport. How to find and automatically scroll to the selected TreeView node, even if it is not visible on the screen?

Sometimes I may load additional TreeView data and refresh the TreeView. Then, I need to scroll to a specific item. Users should not scroll manually in this case.

How to implement behavior like scrollIntoView in Javascript, or BringIntoView in WPF, or EnsureVisible in Windows UI?

Solution

Use the scrollIntoView JavaScript method, which all DOM elements have.

The whole process involves these steps:

  1. Get the TreeView item from the data.
  2. Load additional items on demand, if necessary.
  3. Expand parent(s) via ExpandedItems, if necessary. Older UI for Blazor versions (up to 2.30) require a different implementation with an Expanded property of the TreeView items.
  4. Select the item or implement some way to find it in the DOM.
  5. Set a boolean flag and use it in OnAfterRenderAsync to execute the JavaScript.
  6. Execute the JavaScript code with some timeout to ensure that the new HTML markup has been rendered by the browser. The timeout value will depend on the server-client latency and number of newly rendered items.

Here are two examples:

Load All Data Initially

@inject IJSRuntime js

<label>
    Item (1 - 50):
    <TelerikNumericTextBox @bind-Value="@TreeItemId" Min="1" Max="50" Decimals="0" Width="80px" />
</label>

<TelerikButton OnClick="@SelectAndScroll">Select and Scroll</TelerikButton>

<TelerikTreeView Data="@TreeData"
                 SelectionMode="@TreeViewSelectionMode.Single"
                 @bind-SelectedItems="@SelectedItems"
                 @bind-ExpandedItems="@ExpandedItems"
                 Class="scrollable-treeview">
    <TreeViewBindings>
        <TreeViewBinding />
    </TreeViewBindings>
</TelerikTreeView>

<style>
    .scrollable-treeview {
        height: 300px;
        overflow: auto;
        border: 1px solid #ccc;
        margin: 1em;
    }
</style>

@* ! Move the JavaScript code to its proper place ! *@
<script suppress-error="BL9992">
    function scrollToItem(treeSelector) {
        setTimeout(function() {
            var item = document.querySelector(treeSelector + " .k-selected");
            if (item) {
                item.scrollIntoView();
            }
        }, 300);
    }
</script>

@code {
    List<TreeItem> TreeData { get; set; }
    IEnumerable<object> SelectedItems { get; set; } = new List<TreeItem>();
    IEnumerable<object> ExpandedItems { get; set; } = new List<TreeItem>();

    int? TreeItemId { get; set; } = 37;
    bool ShouldScroll { get; set; }

    void SelectAndScroll()
    {
        if (TreeItemId.HasValue)
        {
            // get the item
            var itemToSelect = TreeData.First(x => x.Id == TreeItemId);

            // get and expand the parent
            if (itemToSelect != null && itemToSelect.ParentId != null)
            {
                var parentItem = TreeData.First(x => x.Id == itemToSelect.ParentId);
                if (parentItem != null)
                {
                    ExpandedItems = ExpandedItems.Append(parentItem);
                }
            }

            // select the item
            SelectedItems = new List<TreeItem>() { itemToSelect };

            // raise flag for JavaScript scrolling
            ShouldScroll = true;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (ShouldScroll)
        {
            ShouldScroll = false;
            await js.InvokeVoidAsync("scrollToItem", ".scrollable-treeview");
        }
        await base.OnAfterRenderAsync(firstRender);
    }

    protected override void OnInitialized()
    {
        TreeData = LoadFlat();
    }

    List<TreeItem> LoadFlat()
    {
        List<TreeItem> items = new List<TreeItem>();

        for (int i = 1; i <= 50; i++)
        {
            var rnd = new Random();
            items.Add(new TreeItem()
            {
                Id = i,
                Text = "Item " + i.ToString(),
                ParentId = i > 3 ? rnd.Next(1, 4) : null,
                HasChildren = i > 3 ? false : true
            });
        }

        return items;
    }

    public class TreeItem
    {
        public int Id { get; set; }
        public string Text { get; set; }
        public int? ParentId { get; set; }
        public bool HasChildren { get; set; }
    }
}

Load Data on Demand

The example uses simplified logic for parent-child item relationship. In production scenarios, you may need to find the correct parent item to expand.

@inject IJSRuntime js

<p>
    <label>
        Root Item (1 - 5):
        <TelerikNumericTextBox @bind-Value="@RootItemId" Min="1" Max="5" Decimals="0" Width="80px" />
    </label>
</p>

<p>
    <label>
        Child Item (1 - 50):
        <TelerikNumericTextBox @bind-Value="@ChildItemId" Min="1" Max="30" Decimals="0" Width="80px" />
    </label>
</p>

<p>
    <label>
        Simulate item load delay (1 - 5000 ms):
        <TelerikNumericTextBox @bind-Value="@LoadingDelay" Min="1" Max="5000" Decimals="0" Width="100px" />
    </label>
</p>

<TelerikButton OnClick="@SelectAndScroll">Select and Scroll</TelerikButton>

<TelerikTreeView Data="@TreeData" OnExpand="@OnTreeViewExpand"
                 SelectionMode="@TreeViewSelectionMode.Single"
                 @bind-SelectedItems="@SelectedItems"
                 @bind-ExpandedItems="@ExpandedItems"
                 Class="scrollable-treeview">
    <TreeViewBindings>
        <TreeViewBinding />
    </TreeViewBindings>
</TelerikTreeView>

<style>
    .scrollable-treeview {
        height: 300px;
        overflow: auto;
        border: 1px solid #ccc;
        margin: 1em;
    }
</style>

@* ! Move the JavaScript code to its proper place ! *@
<script suppress-error="BL9992">
    function scrollToItem(treeSelector) {
        setTimeout(function() {
            var item = document.querySelector(treeSelector + " .k-selected");
            if (item) {
                item.scrollIntoView();
            }
        }, 300);
    }
</script>

@code {
    List<TreeItem> TreeData { get; set; }
    IEnumerable<object> SelectedItems { get; set; } = new List<TreeItem>();
    IEnumerable<object> ExpandedItems { get; set; } = new List<TreeItem>();

    int? RootItemId { get; set; } = 3;
    int? ChildItemId { get; set; } = 25;
    int LoadingDelay { get; set; } = 500;
    bool ShouldScroll { get; set; }

    async Task SelectAndScroll()
    {
        if (RootItemId.HasValue && ChildItemId.HasValue)
        {
            // get the item
            var parentItem = TreeData.First(x => x.Id == RootItemId);
            var itemId = RootItemId * 100 + ChildItemId;
            var itemToSelect = TreeData.FirstOrDefault(x => x.Id == itemId);

            // load data if necessary
            if (itemToSelect == null)
            {
                await LoadChildren(parentItem);
                itemToSelect = TreeData.FirstOrDefault(x => x.Id == itemId);
            }

            // expand the parent
            ExpandedItems = ExpandedItems.Append(parentItem);

            // refresh TreeView after loading children
            TreeData = new List<TreeItem>(TreeData);

            // select the item
            SelectedItems = new List<TreeItem>() { itemToSelect };

            // raise flag for JavaScript scrolling
            ShouldScroll = true;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (ShouldScroll)
        {
            ShouldScroll = false;
            await js.InvokeVoidAsync("scrollToItem", ".scrollable-treeview");
        }
        await base.OnAfterRenderAsync(firstRender);
    }

    async Task OnTreeViewExpand(TreeViewExpandEventArgs args)
    {
        var item = args.Item as TreeItem;

        if (args.Expanded && !TreeData.Any(x => x.ParentId == item.Id))
        {
            await LoadChildren(item);
        }
    }

    async Task LoadChildren(TreeItem item)
    {
        var parentId = item.Id;

        // simulate network delay
        await Task.Delay(LoadingDelay);

        for (int i = parentId * 100 + 1; i <= parentId * 100 + 30; i++)
        {
            TreeData.Add(new TreeItem()
            {
                Id = i,
                Text = $"Item {parentId} - {i}",
                ParentId = parentId
            });
        }
    }

    protected override void OnInitialized()
    {
        TreeData = LoadFlat();
    }

    List<TreeItem> LoadFlat()
    {
        List<TreeItem> items = new List<TreeItem>();

        for (int i = 1; i <= 5; i++)
        {
            items.Add(new TreeItem()
            {
                Id = i,
                Text = "Item " + i.ToString(),
                ParentId = null,
                HasChildren = true
            });
        }

        return items;
    }

    public class TreeItem
    {
        public int Id { get; set; }
        public string Text { get; set; }
        public int? ParentId { get; set; }
        public bool HasChildren { get; set; }
    }
}
In this article