Scroll to TreeView Item
Environment
Product | TreeView for Blazor |
Description
This KB article discusses the following scenarios and questions:
- 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 TreeView item automatically and without a need for manual user interaction.
- How to implement behavior like
scrollIntoView
in Javascript, orBringIntoView
in WPF, orEnsureVisible
in Windows UI? - How to focus a TreeView node, so that the user doesn't have to scroll to it?
Solution
Use the scrollIntoView
JavaScript method, which all DOM elements have.
The whole process involves these steps:
- Get the TreeView item from the data.
- Load additional items on demand, if necessary.
- Expand parent(s) via
ExpandedItems
, if necessary. Older UI for Blazor versions (up to 2.30) require a different implementation with anExpanded
property of the TreeView items. - Select the item or implement some way to find it in the DOM.
- Set a boolean flag and use it in
OnAfterRenderAsync
to execute the JavaScript. - 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.
- (Optional) Focus the TreeView item element (
<li class="k-treeview-item">
) to continue the keyboard navigation from that item.
Here are two examples:
- The first TreeView receives all data initially.
- The second TreeView loads child items on demand.
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;
scroll-behavior: smooth;
border: 1px solid #ccc;
margin: 1em;
}
</style>
@* Move the JavaScript to a separate JS file *@
<script suppress-error="BL9992">
function scrollToItem(treeSelector) {
setTimeout(function() {
var item = document.querySelector(treeSelector + " .k-selected");
if (item) {
// scroll to item
item.scrollIntoView({ block: "nearest" });
// focus item for keyboard navigation
item.closest(".k-treeview-item").focus();
}
});
}
</script>
@code {
private List<TreeItem> TreeData { get; set; } = new();
private IEnumerable<object> SelectedItems { get; set; } = new List<TreeItem>();
private IEnumerable<object> ExpandedItems { get; set; } = new List<TreeItem>();
private int TreeItemId { get; set; } = 44;
private bool ShouldScroll { get; set; }
private void SelectAndScroll()
{
// get the item
TreeItem? itemToSelect = TreeData.FirstOrDefault(x => x.Id == TreeItemId);
if (itemToSelect != null)
{
if (itemToSelect?.ParentId != null)
{
// get and expand the parent
var parentItem = TreeData.First(x => x.Id == itemToSelect.ParentId);
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;
// wait for the TreeView item to select
await Task.Delay(1);
await js.InvokeVoidAsync("scrollToItem", ".scrollable-treeview");
}
await base.OnAfterRenderAsync(firstRender);
}
protected override void OnInitialized()
{
TreeData = LoadFlat();
}
private 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}",
ParentId = i > 3 ? rnd.Next(1, 4) : null,
HasChildren = i <= 3
});
}
return items;
}
public class TreeItem
{
public int Id { get; set; }
public string Text { get; set; } = string.Empty;
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) {
// scroll to item
item.scrollIntoView({ block: "nearest" });
// focus item for keyboard navigation
item.closest(".k-treeview-item").focus();
}
}, 300);
}
</script>
@code {
private List<TreeItem> TreeData { get; set; }
private IEnumerable<object> SelectedItems { get; set; } = new List<TreeItem>();
private IEnumerable<object> ExpandedItems { get; set; } = new List<TreeItem>();
private int? RootItemId { get; set; } = 3;
private int? ChildItemId { get; set; } = 25;
private int LoadingDelay { get; set; } = 500;
private bool ShouldScroll { get; set; }
private 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);
}
private async Task OnTreeViewExpand(TreeViewExpandEventArgs args)
{
var item = args.Item as TreeItem;
if (args.Expanded && !TreeData.Any(x => x.ParentId == item.Id))
{
await LoadChildren(item);
}
}
private 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();
}
private 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; }
}
}