TreeList Popup Editing
In this article:
Basics
Popup editing lets the user click an Edit command button on the row, and a popup shows up with all its editable columns open up for changes. They can then click the Save
button in the dialog to submit the changes to the model. This fires the OnUpdate
event of the treelist where your code receives the updated model so you can work with the data (for example, to call the appropriate method of your service).
In a similar fashion, the Cancel
, Delete
command buttons and the Add
toolbar button fire events on the treelist to let you handle the data source operations.
You can also cancel the events by setting the IsCancelled
property of the event arguments to true
. This lets you prevent the user from editing certain records, inserting or deleting items, based on your application logic.
To enable Popup editing in the treelist, set its EditMode
property to Telerik.Blazor.TreeListEditMode.Popup
, then handle the CRUD events as shown in the example below.
The Popup editing mode supports validation. To use it, all you need to do is decorate your model with the desired annotations. Validation errors will be shown in the popup and will prevent the Update operation.
@using System.ComponentModel.DataAnnotations
@* Used for the model annotations only *@
Editing is cancelled for the first record.
<br />
<TelerikTreeList Data="@Data"
EditMode="@TreeListEditMode.Popup"
OnUpdate="@UpdateItem"
OnDelete="@DeleteItem"
OnCreate="@CreateItem"
OnEdit="@OnEditHandler"
OnCancel="@OnCancelHandler"
Pageable="true" ItemsField="@(nameof(Employee.DirectReports))"
Width="850px">
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add</TreeListCommandButton>
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListCommandColumn Width="280px">
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Child</TreeListCommandButton>
<TreeListCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</TreeListCommandButton>
<TreeListCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Update</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</TreeListCommandButton>
</TreeListCommandColumn>
<TreeListColumn Field="Name" Expandable="true" Width="320px" />
<TreeListColumn Field="Id" Editable="false" Width="120px" />
<TreeListColumn Field="EmailAddress" Width="220px" />
<TreeListColumn Field="HireDate" Width="220px" />
</TreeListColumns>
</TelerikTreeList>
@code {
public List<Employee> Data { get; set; }
// Sample CUD operations for the local data
async Task UpdateItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
// perform actual data source operations here through your service
await MyService.Update(item);
// update the local view-model data with the service data
await GetTreeListData();
}
async Task CreateItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
var parentItem = args.ParentItem as Employee;
// perform actual data source operations here through your service
await MyService.Create(item, parentItem);
// update the local view-model data with the service data
await GetTreeListData();
}
async Task DeleteItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
// perform actual data source operations here through your service
await MyService.Delete(item);
// update the local view-model data with the service data
await GetTreeListData();
}
// OnEdit handler
async Task OnEditHandler(TreeListCommandEventArgs args)
{
Employee empl = args.Item as Employee;
if (empl.Id == 1)
{
// prevent opening for edit based on condition
args.IsCancelled = true;
Console.WriteLine("You cannot edit this item");
}
}
// OnCancel handler
async Task OnCancelHandler(TreeListCommandEventArgs args)
{
Employee empl = args.Item as Employee;
// if necessary, perform actual data source operation here through your service
}
// sample model
public class Employee
{
public int Id { get; set; }
[Required(ErrorMessage = "The employee must have a name")]
public string Name { get; set; }
[EmailAddress]
public string EmailAddress { get; set; }
public DateTime HireDate { get; set; }
public List<Employee> DirectReports { get; set; }
public bool HasChildren { get; set; }
// Used for the editing so replacing the object in the view-model data
// will treat it as the same object and keep its state - otherwise it will
// collapse after editing is done, which is not what the user would expect
public override bool Equals(object obj)
{
if (obj is Employee)
{
return this.Id == (obj as Employee).Id;
}
return false;
}
}
// data generation
async Task GetTreeListData()
{
Data = await MyService.Read();
}
protected override async Task OnInitializedAsync()
{
await GetTreeListData();
}
// the following static class mimics an actual data service that handles the actual data source
// replace it with your actual service through the DI, this only mimics how the API can look like and works for this standalone page
public static class MyService
{
private static List<Employee> _data { get; set; } = new List<Employee>();
// used in this example for data generation and retrieval for CUD operations on the current view-model data
private static int LastId { get; set; } = 1;
public static async Task Create(Employee itemToInsert, Employee parentItem)
{
InsertItemRecursive(_data, itemToInsert, parentItem);
}
public static async Task<List<Employee>> Read()
{
if (_data.Count < 1)
{
for (int i = 1; i < 15; i++)
{
Employee root = new Employee
{
Id = LastId,
Name = $"root: {i}",
EmailAddress = $"{i}@example.com",
HireDate = DateTime.Now.AddYears(-i),
DirectReports = new List<Employee>(),
HasChildren = true
};
_data.Add(root);
LastId++;
for (int j = 1; j < 4; j++)
{
int currId = LastId;
Employee firstLevelChild = new Employee
{
Id = currId,
Name = $"first level child {j} of {i}",
EmailAddress = $"{currId}@example.com",
HireDate = DateTime.Now.AddDays(-currId),
DirectReports = new List<Employee>(),
HasChildren = true
};
root.DirectReports.Add(firstLevelChild);
LastId++;
for (int k = 1; k < 3; k++)
{
int nestedId = LastId;
firstLevelChild.DirectReports.Add(new Employee
{
Id = LastId,
Name = $"second level child {k} of {j} and {i}",
EmailAddress = $"{nestedId}@example.com",
HireDate = DateTime.Now.AddMinutes(-nestedId)
}); ;
LastId++;
}
}
}
_data[0].Name += " (non-editable, see OnEdit)";
}
return await Task.FromResult(_data);
}
public static async Task Update(Employee itemToUpdate)
{
UpdateItemRecursive(_data, itemToUpdate);
}
public static async Task Delete(Employee itemToDelete)
{
RemoveChildRecursive(_data, itemToDelete);
}
// sample helper methods for handling the view-model data hierarchy
static void UpdateItemRecursive(List<Employee> items, Employee itemToUpdate)
{
for (int i = 0; i < items.Count; i++)
{
if (items[i].Id.Equals(itemToUpdate.Id))
{
items[i] = itemToUpdate;
return;
}
if (items[i].DirectReports?.Count > 0)
{
UpdateItemRecursive(items[i].DirectReports, itemToUpdate);
}
}
}
static void RemoveChildRecursive(List<Employee> items, Employee item)
{
for (int i = 0; i < items.Count(); i++)
{
if (item.Equals(items[i]))
{
items.Remove(item);
return;
}
else if (items[i].DirectReports?.Count > 0)
{
RemoveChildRecursive(items[i].DirectReports, item);
if (items[i].DirectReports.Count == 0)
{
items[i].HasChildren = false;
}
}
}
}
static void InsertItemRecursive(List<Employee> Data, Employee insertedItem, Employee parentItem)
{
insertedItem.Id = LastId++;
if (parentItem != null)
{
parentItem.HasChildren = true;
if (parentItem.DirectReports == null)
{
parentItem.DirectReports = new List<Employee>();
}
parentItem.DirectReports.Insert(0, insertedItem);
}
else
{
Data.Insert(0, insertedItem);
}
}
}
}
It is up to the data access logic to save the data once it is changed in the data collection, or to revert changes. The example above showcases the events that allow you to do that. In a real application, the code for handling data operations may be entirely different.
Customization
The TreeList exposes options to customize the edit popup and its form. You can define your desired configuration in the TreeListPopupEditSettings
and TreeListPopupEditFormSettings
tags under the TreeListSettings
tag.
Popup Customization
The TreeListPopupEditSettings
nested tag exposes the following parameters to allow popup customization:
Parameter | Type | Description |
---|---|---|
Class |
string |
The CSS class of the edit popup |
Title |
string |
The title of the edit popup |
ThemeColor |
string |
The color scheme of the window. Use the available members of the static class ThemeConstants.Window.ThemeColor . |
Width |
string |
The Width of the edit popup |
MaxWidth |
string |
The maximum width of the window |
MinWidth |
string |
The minimum width of the window |
Height |
string |
The height of edit popup |
MaxHeight |
string |
The maximum height of the window |
MinHeight |
string |
The minimum height of the window |
The min/max options for the width and height apply to the initial rendering of the window and not browser resizing.
For example, here is how to set the TreeList popup edit form's title, so that it matches a property value of the edited data item.
Edit Form Customization
The TreeListPopupEditFormSettings
nested tag exposes the following parameters to allow edit form customization:
Parameter | Type and Default Value | Description |
---|---|---|
ButtonsLayout |
FormButtonsLayout ( End ) |
The horizontal alignment of the buttons. Takes a member of the FormButtonsLayout enum: - Start - End - Center - Stretch
|
Columns |
int |
The number of the form columns |
ColumnSpacing |
int |
The column spacing |
Orientation |
FormOrientation ( Vertical ) |
The orientation of the form. Takes a member of the FormOrientation enum: - Horizontal - Vertical
|
@*The snippet focuses on the popup edit form customization. CRUD events are not handled for brevity*@
<TelerikTreeList Data="@Data" Pageable="true"
EditMode="@TreeListEditMode.Popup"
ItemsField="@(nameof(Employee.DirectReports))">
<TreeListSettings>
<TreeListPopupEditSettings Width="700px"
MinWidth="650px"
MaxHeight="300px"
Class="custom-popup">
</TreeListPopupEditSettings>
<TreeListPopupEditFormSettings Orientation="@FormOrientation.Horizontal"
ButtonsLayout="FormButtonsLayout.Stretch"
Columns="2">
</TreeListPopupEditFormSettings>
</TreeListSettings>
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add</TreeListCommandButton>
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListColumn Field="Id" Width="120px" />
<TreeListColumn Field="Name" Expandable="true" />
<TreeListColumn Field="EmailAddress" />
<TreeListColumn Field="HireDate" />
<TreeListCommandColumn Width="280px">
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Child</TreeListCommandButton>
<TreeListCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</TreeListCommandButton>
<TreeListCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Update</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</TreeListCommandButton>
</TreeListCommandColumn>
</TreeListColumns>
</TelerikTreeList>
@code {
public List<Employee> Data { get; set; }
// sample model
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
public DateTime HireDate { get; set; }
public List<Employee> DirectReports { get; set; }
public bool HasChildren { get; set; }
}
// data generation
public int LastId { get; set; } = 1;
protected override async Task OnInitializedAsync()
{
Data = await GetTreeListData();
}
async Task<List<Employee>> GetTreeListData()
{
List<Employee> data = new List<Employee>();
for (int i = 1; i < 15; i++)
{
Employee root = new Employee
{
Id = LastId,
Name = $"root: {i}",
EmailAddress = $"{i}@example.com",
HireDate = DateTime.Now.AddYears(-i),
DirectReports = new List<Employee>(),
HasChildren = true
};
data.Add(root);
LastId++;
for (int j = 1; j < 4; j++)
{
int currId = LastId;
Employee firstLevelChild = new Employee
{
Id = currId,
Name = $"first level child {j} of {i}",
EmailAddress = $"{currId}@example.com",
HireDate = DateTime.Now.AddDays(-currId),
DirectReports = new List<Employee>(),
HasChildren = true
};
root.DirectReports.Add(firstLevelChild);
LastId++;
for (int k = 1; k < 3; k++)
{
int nestedId = LastId;
firstLevelChild.DirectReports.Add(new Employee
{
Id = LastId,
Name = $"second level child {k} of {j} and {i}",
EmailAddress = $"{nestedId}@example.com",
HireDate = DateTime.Now.AddMinutes(-nestedId)
}); ;
LastId++;
}
}
}
return await Task.FromResult(data);
}
}
Popup Form Customization
In the TreeListPopupEditFormSettings
, you can declare a FormTemplate
. This template enables you to fully customize the appearance and content of the create/edit Popup window in the TreeList. For more information and examples on customizing the TreeList Popup window, refer to the Popup Form Template article.
Popup Buttons Customization
You can specify a ButtonsTemplate
in the TreeListPopupEditFormSettings
to customize how the buttons look in the create/edit Popup window of the TreeList. To learn more and see an example of customizing the TreeList Popup buttons, refer to the Popup Buttons Template article.