Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions knowledge-base/promptbox-scroll-to-bottom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
title: How to Automatically Scroll to Bottom
description: Learn how to implement automatic scroll to bottom, after the user submits a message, or a responce is received.
type: how-to
page_title: How to Implement Automatic Scroll to Bottom when using the Telerik UI for Blazor PromptBox and a Messages Container
slug: promptbox-kb-scroll-to-bottom
tags: blazor, popover, scroll, bottom
ticketid: 1714794
res_type: kb
components: ["promptbox"]
---

## Environment

<table>
<tbody>
<tr>
<td>Product</td>
<td>PromptBox for Blazor</td>
</tr>
</tbody>
</table>

## Description

This KB shows how to implement automatic scroll to bottom in the PromptBox, after the user submits a message, or a responce is received.
Comment thread
IvanDanchev marked this conversation as resolved.

## Solution

1. Add a `<div>` HTML element that will serve as a scrollable container for the messages. Set its `id` attribute and apply `overflow-y: auto;`.
````RAZOR.skip-repl
<div id="message-list" style="flex:1; overflow-y:auto; padding:1rem; display:flex; flex-direction:column; gap:0.75rem;">
````

2. Use JS interop to invoke a JavaScript function that sets the scroll position of the messages container.
Comment thread
IvanDanchev marked this conversation as resolved.
````RAZOR.skip-repl
await JS.InvokeVoidAsync("scrollToBottom", "message-list");
````

3. Use a flag to schedule scrolling after each render. In `ScrollAsync`, set the flag and call `StateHasChanged()`. In `OnAfterRenderAsync`, check the flag, reset it, and invoke the JavaScript function:
````RAZOR.skip-repl
private bool ShouldScroll;

private void ScrollAsync()
{
ShouldScroll = true;
StateHasChanged();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (ShouldScroll)
{
ShouldScroll = false;
await JS.InvokeVoidAsync("scrollToBottom", "message-list");
}
}
````

>caption Scroll to bottom in the PromptBox

````RAZOR
@inject IJSRuntime JS

<div style="max-width:700px; margin:2rem auto; display:flex; flex-direction:column; height:80vh;">

<div id="message-list" style="flex:1; overflow-y:auto; padding:1rem; display:flex; flex-direction:column; gap:0.75rem;">
@foreach (var msg in Messages)
{
<div style="display:flex; justify-content:@(msg.IsUser ? "flex-end" : "flex-start")">
<div style="max-width:70%; padding:0.6rem 1rem; border-radius:12px;
background:@(msg.IsUser ? "#0078d4" : "#f3f3f3");
color:@(msg.IsUser ? "white" : "#1a1a1a");
font-size:0.95rem; line-height:1.5; word-break:break-word;">
@msg.Text
</div>
</div>
}
@if (IsLoading)
{
<div style="display:flex; justify-content:flex-start">
<div style="padding:0.6rem 1rem; border-radius:12px; background:#f3f3f3; color:#888; font-style:italic;">
AI is thinking…
</div>
</div>
}
</div>

<div style="padding-top:0.5rem;">
<TelerikPromptBox @bind-Value="@Prompt"
Mode="PromptBoxMode.Auto"
IsLoading="@IsLoading"
Placeholder="Type a message…"
OnPromptAction="@OnActionButtonClick" />
</div>
</div>

<script suppress-error="BL9992">
window.scrollToBottom = function (id) {
const el = document.getElementById(id);
if (el) el.scrollTop = el.scrollHeight;
};
</script>

@code {
private string Prompt = string.Empty;
private bool IsLoading { get; set; }
private bool ShouldScroll;

private List<ChatMessage> Messages { get; set; } = new()
{
new ChatMessage(false, "Hello! How can I help you today?"),
new ChatMessage(true, "What is Blazor?"),
new ChatMessage(false, "Blazor is a free and open-source web framework that enables developers to create web apps using C# and HTML. It is part of the ASP.NET Core ecosystem."),
new ChatMessage(true, "Can I use it with Telerik components?"),
new ChatMessage(false, "Absolutely! Telerik UI for Blazor provides a rich set of native Blazor components — grids, charts, inputs, and more — that work seamlessly with both Blazor Server and Blazor WebAssembly.")
};

private static readonly string[] SimulatedReplies =
{
"That's a great question! Let me think about that for a moment…",
"Based on what you said, I'd suggest reviewing the official documentation.",
"Interesting point! In Blazor, you can achieve this with dependency injection and component parameters.",
"Sure thing! The Telerik Grid supports sorting, filtering, paging, and virtual scrolling out of the box.",
"You can use @bind-Value for two-way data binding in any Telerik input component.",
"Great choice! Blazor WebAssembly runs entirely in the browser using WebAssembly.",
"I recommend checking the Telerik demos at demos.telerik.com/blazor-ui for live examples."
};

private async Task OnActionButtonClick(PromptBoxActionButtonEventArgs args)
{
if (args.Action == PromptBoxActionType.Stop)
{
IsLoading = false;
return;
}

if (args.Action == PromptBoxActionType.Send && !string.IsNullOrWhiteSpace(args.Text))
{
Messages.Add(new ChatMessage(true, args.Text));
Prompt = string.Empty;
IsLoading = true;

ScrollAsync();

await Task.Delay(Random.Shared.Next(800, 1800));

Messages.Add(new ChatMessage(false, SimulatedReplies[Random.Shared.Next(SimulatedReplies.Length)]));
IsLoading = false;

ScrollAsync();
}
}

private void ScrollAsync()
{
ShouldScroll = true;
StateHasChanged();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (ShouldScroll)
{
ShouldScroll = false;
await JS.InvokeVoidAsync("scrollToBottom", "message-list");
}
}

private record ChatMessage(bool IsUser, string Text);
}
````

## See Also

* [PromptBox Events](slug:promptbox-events)
* [PromptBox Overview](slug:promptbox-overview)
206 changes: 206 additions & 0 deletions knowledge-base/treelist-expand-parent-nodes-when-filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
title: How to Expand Parent Nodes to Show Filtered Children
description: Learn how to expand TreeList parent nodes after filtering, to display the filtered child nodes.
type: how-to
slug: treelist-kb-expand-parent-nodes-filtering
tags: telerik, blazor, treelist, filter, expand
ticketid: 1715942
res_type: kb
components: ["treelist"]
---

## Environment

<table>
<tbody>
<tr>
<td>Product</td>
<td>TreeList for Blazor</td>
</tr>
</tbody>
</table>

## Description

This KB shows how to expand parent nodes in the TreeList, to display their child nodes returned after filtering.

## Solution

1. Implement a [FilterCellTemplate](slug:treelist-templates-filter#filter-row-template) and add a TextBox component to it.
2. Handle the [ValueChanged](slug:components/textbox/events#valuechanged) event of the TextBox and implement logic that walks the full tree, finds matches, and collects their ancestors.
3. Set `state.ExpandedItems` to the list of ancestors, and call `SetStateAsync()`.
4. Use the `OnStateChanged` event to detect when the user expands/collapses a parent node, so that you can restore the parent's state after the filter is cleared.

>caption Expand parent nodes after filtering their children

````RAZOR
@using Telerik.DataSource

<TelerikTreeList @ref="TreeListRef"
Comment thread
IvanDanchev marked this conversation as resolved.
Data="@Locations"
ItemsField="@nameof(Location.Children)"
FilterMode="TreeListFilterMode.FilterRow"
OnStateChanged="@((TreeListStateEventArgs<Location> args) => OnStateChanged(args))">
<TreeListColumns>
<TreeListColumn Field="@nameof(Location.Name)" Title="Name" Expandable="true" Width="300px">
<FilterCellTemplate Context="filterCtx">
<TelerikTextBox Value="@FilterText"
ValueChanged="@((string v) => OnFilterChanged(v, filterCtx))"
AutoComplete="off"
Placeholder="Search…" />
@if (!string.IsNullOrEmpty(FilterText))
{
<TelerikButton Icon="@FontIcon.FilterClear" OnClick="@(() => OnFilterChanged(string.Empty, filterCtx))" />
}
</FilterCellTemplate>
</TreeListColumn>
<TreeListColumn Field="@nameof(Location.Country)" Title="Country" Width="150px" />
</TreeListColumns>
</TelerikTreeList>

@code {
private TelerikTreeList<Location> TreeListRef;
private string FilterText { get; set; } = string.Empty;
// Tracks which items the user manually expanded, so that they can keep their state after clearing the filter.
private HashSet<Location> UserExpandedItems = new();
private List<Location> Locations { get; set; } = new();

protected override void OnInitialized()
{
Locations = new List<Location>
{
new Location { Id = 1, Name = "Asia", Country = "", Children = new()
{
new Location { Id = 2, Name = "Japan", Country = "JP", Children = new()
{
new Location { Id = 3, Name = "Tokyo", Country = "JP" },
new Location { Id = 4, Name = "Osaka", Country = "JP" },
new Location { Id = 5, Name = "Kyoto", Country = "JP" }
}},
new Location { Id = 6, Name = "China", Country = "CN", Children = new()
{
new Location { Id = 7, Name = "Beijing", Country = "CN" },
new Location { Id = 8, Name = "Shanghai", Country = "CN" }
}},
new Location { Id = 9, Name = "India", Country = "IN", Children = new()
{
new Location { Id = 10, Name = "Mumbai", Country = "IN" },
new Location { Id = 11, Name = "Delhi", Country = "IN" }
}}
}},
new Location { Id = 12, Name = "Europe", Country = "", Children = new()
{
new Location { Id = 13, Name = "Germany", Country = "DE", Children = new()
{
new Location { Id = 14, Name = "Berlin", Country = "DE" },
new Location { Id = 15, Name = "Munich", Country = "DE" }
}},
new Location { Id = 16, Name = "France", Country = "FR", Children = new()
{
new Location { Id = 17, Name = "Paris", Country = "FR" },
new Location { Id = 18, Name = "Lyon", Country = "FR" }
}}
}},
new Location { Id = 19, Name = "Americas", Country = "", Children = new()
{
new Location { Id = 20, Name = "United States", Country = "US", Children = new()
{
new Location { Id = 21, Name = "New York", Country = "US" },
new Location { Id = 22, Name = "Los Angeles", Country = "US" }
}},
new Location { Id = 23, Name = "Brazil", Country = "BR", Children = new()
{
new Location { Id = 24, Name = "São Paulo", Country = "BR" },
new Location { Id = 25, Name = "Rio de Janeiro", Country = "BR" }
}}
}}
};
}

private async Task OnFilterChanged(string newValue, FilterCellTemplateContext filterCtx)
{
FilterText = newValue;

// Push the value into the TreeList's built-in filter pipeline.
var descriptor = filterCtx.FilterDescriptor.FilterDescriptors
.OfType<FilterDescriptor>()
.FirstOrDefault();

if (descriptor != null)
{
descriptor.Value = string.IsNullOrEmpty(newValue) ? null : (object)newValue;
descriptor.Operator = FilterOperator.Contains;
}
await filterCtx.FilterAsync();

// Now update expansion via state.
var state = TreeListRef.GetState();

if (string.IsNullOrEmpty(newValue))
{
// Restore user's manual expansion when the filter is cleared.
state.ExpandedItems = UserExpandedItems.ToList();
}
else
{
var ancestorsToExpand = new HashSet<Location>();
foreach (var root in Locations)
CollectAncestorsOfMatches(root, newValue, ancestorsToExpand);

state.ExpandedItems = ancestorsToExpand.ToList();
}

await TreeListRef.SetStateAsync(state);
}

private void OnStateChanged(TreeListStateEventArgs<Location> args)
{
// Keep `UserExpandedItems` in sync with manual expand/collapse,
// but only when no filter is active (during filtering, expansion is driven by us).
if (string.IsNullOrEmpty(FilterText))
{
UserExpandedItems = args.TreeListState.ExpandedItems?.ToHashSet() ?? new();
}
}

/// <summary>
/// Recursively walks the subtree. Returns true if this node or any descendant
/// matches the filter. Adds any node that has a matching descendant to <paramref name="ancestors"/>
/// so it will be expanded.
/// </summary>
private bool CollectAncestorsOfMatches(Location node, string filter, HashSet<Location> ancestors)
{
bool selfMatches = node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase);

bool anyChildMatches = false;
if (node.Children != null)
{
foreach (var child in node.Children)
{
if (CollectAncestorsOfMatches(child, filter, ancestors))
anyChildMatches = true;
}
}

if (anyChildMatches)
ancestors.Add(node);

return selfMatches || anyChildMatches;
}

public class Location
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public List<Location>? Children { get; set; }
}
}
````

## See Also

* [TreeList State Documentation](slug:treelist-state)
* [TreeList Filtering](slug:treelist-filtering)
* [TreeList FilterCellTemplate](slug:treelist-templates-filter#filter-row-template)
* [TreeList Overview](slug:treelist-overview)
Loading