-
Notifications
You must be signed in to change notification settings - Fork 78
Kb july 2026 #3775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Kb july 2026 #3775
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| ## 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. | ||
|
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
206
knowledge-base/treelist-expand-parent-nodes-when-filtering.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
|
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) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.