Skip to content

Commit

Permalink
feat(TreeView): support virtualize scroll (#4624)
Browse files Browse the repository at this point in the history
* feat: 增加 IsVirtual 参数

* doc: 增加大数据示例

* doc: 增加虚拟滚动文档

* feat: 节点文本增加不折行样式

* doc: 更新示例

* refactor: 代码格式化

* refactor: 加载子节点动画改为脚本实现

* doc: 虚拟滚动模式下手风琴特效禁用

* refactor(Checkbox): 代码格式化

* fix(Checkbox): 修复点击未刷新问题

* refactor: 增加 @ 关键字

* chore: 更新依赖

* refactor: 客户端实现级联操作

* doc: 更新示例

* feat: 实现客户端更新父节点逻辑

* refactor: 优化性能

* refactor: 增加父节点逻辑判断

* perf: 增加内部缓存提高性能

* feat: 增加 TreeView 加载动画图标

* refactor: 增加加载动画图标样式

* refactor: 增加加载动画样式脚本

* doc: 增加加载子节点动画延时

* refactor: 子节点展开清缓存

* wip: 临时提交

* feat: 实现父节点级联逻辑

* refactor: 使用原生 checkbox 元素

* feat: 增加 TriggerClick 逻辑改为客户端触发

* feat: 实现客户端脚本逻辑

* feat: 增加父节点样式

* refactor: 移除父节点样式

* feat: 增加同步样式代码

* refactor: 精简回调逻辑提高性能

* refactor: 更新客户端逻辑仅回调一次

* refactor: 取消 Checkbox 冒泡限制

* feat: 增加当前节点状态同步逻辑

* fix: 子节点联动逻辑增加本身状态改变

* refactor: 移除 SetNodeStateByIndex 方法提高性能

* fix: 修复父节点级联状态未同步问题

* refactor: 微调逻辑

* test: 更新单元测试

* revert: 撤销更改

* refactor: 不需要等待客户端脚本执行

* refactor: 重构递归方法

* refactor: 重构 SetParentCheck 方法提高性能

* test: 更新单元测试

* test: 更新单元测试

* refactor: 移除关键字

* test: 更新单元测试

* test: 修复单元测试

* test: 修复单元测试

* test: 更新单元测试

* refactor: 增加 net9.0 新中间件

* chore: 更新配置开发模式使用单框架

* chore: App 引导页支持静态资源

* chore: 增加 RunTargetFramework 配置项

* chore: App 支持切换 net8/9

* refactor: 更新级联设置子节点逻辑

* test: 更新单元测试

* refactor: 重构方法提高性能

* refactor: 代码格式化

* test: 更新单元测试

* test: 补充单元测试

* test: 增加虚拟化单元测试

* test: 增加 SetParentCheck 单元测试

* test: 增加单元测试

* test: 更新单元测试

* refactor: 重构 OnTriggerClickAsync

* test: 更新单元测试
  • Loading branch information
ArgoZhang authored Nov 9, 2024
1 parent 3cbd726 commit 17ed9ac
Show file tree
Hide file tree
Showing 18 changed files with 668 additions and 257 deletions.
1 change: 1 addition & 0 deletions exclusion.dic
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Segmenteds
Responsives
resx
tabset
tabindex
Splittings
Foos
Localizer
Expand Down
92 changes: 46 additions & 46 deletions src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,52 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BootstrapBlazor.AntDesignIcon" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.AzureOpenAI" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.AzureTranslator" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BaiduSpeech" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BaiduOcr" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BarCode" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BarcodeGenerator" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BootstrapIcon" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.BootstrapIcon.Extensions" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.CherryMarkdown" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Dock" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.DockView" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.DriverJs" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.ElementIcon" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.FileViewer" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Gantt" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Holiday" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Html2Pdf" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.IconPark" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.ImageCropper" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Live2DDisplay" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Markdown" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.MaterialDesign" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.MaterialDesign.Extensions" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.MeiliSearch" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Middleware" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.MindMap" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.MouseFollower" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.OnScreenKeyboard" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.PdfReader" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Player" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.SignaturePad" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Sortable" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Splitting" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.SvgEditor" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.SummerNote" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.TagHelper" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.Topology" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.VideoPlayer" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.0-beta02" />
<PackageReference Include="Longbow.Logging" Version="9.0.0-beta01" />
<PackageReference Include="Longbow.Tasks" Version="9.0.0-beta01" />
<PackageReference Include="BootstrapBlazor.AntDesignIcon" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.AzureOpenAI" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.AzureTranslator" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BaiduSpeech" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BaiduOcr" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BarCode" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BarcodeGenerator" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BootstrapIcon" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.BootstrapIcon.Extensions" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.CherryMarkdown" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.CodeEditor" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Dock" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.DockView" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.DriverJs" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.ElementIcon" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.FileViewer" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Gantt" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Holiday" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Html2Pdf" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.IconPark" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.ImageCropper" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Live2DDisplay" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Markdown" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.MaterialDesign" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.MaterialDesign.Extensions" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.MeiliSearch" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Middleware" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.MindMap" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.MouseFollower" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.OnScreenKeyboard" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.PdfReader" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Player" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.SignaturePad" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Sortable" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Splitting" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.SvgEditor" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.SummerNote" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.TagHelper" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.Topology" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.VideoPlayer" Version="9.0.0-beta*" />
<PackageReference Include="BootstrapBlazor.WinBox" Version="9.0.0-beta*" />
<PackageReference Include="Longbow.Logging" Version="9.0.0-beta*" />
<PackageReference Include="Longbow.Tasks" Version="9.0.0-beta*" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 13 additions & 0 deletions src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@
<p>@((MarkupString)Localizer["TreeViewsTips11"].Value)</p>
<p>@((MarkupString)Localizer["TreeViewsTips12"].Value)</p>

<DemoBlock Title="@Localizer["TreeViewVirtualizeTitle"]"
Introduction="@Localizer["TreeViewVirtualizeIntro"]"
Name="DefaultExpand">
<section ignore>
@((MarkupString)Localizer["TreeViewVirtualizeDescription"].Value)
</section>
<div style="height: 400px">
<TreeView TItem="TreeFoo" Items="@VirtualizeItems" ShowCheckbox="true" IsVirtualize="true"
AutoCheckChildren="true" AutoCheckParent="true"
OnExpandNodeAsync="OnExpandVirtualNodeAsync"></TreeView>
</div>
</DemoBlock>

<DemoBlock Title="@Localizer["TreeViewNormalTitle"]"
Introduction="@Localizer["TreeViewNormalIntro"]"
Name="Normal">
Expand Down
38 changes: 22 additions & 16 deletions src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,38 @@ public sealed partial class TreeViews
private bool DisableCanExpand { get; set; }
private bool IsDisabled { get; set; }

private List<TreeViewItem<TreeFoo>> Items { get; set; } = TreeFoo.GetTreeItems();
private List<TreeViewItem<TreeFoo>> Items { get; } = TreeFoo.GetTreeItems();

private bool AutoCheckChildren { get; set; }

private bool AutoCheckParent { get; set; }

private List<TreeViewItem<TreeFoo>> DisabledItems { get; set; } = GetDisabledItems();
private List<TreeViewItem<TreeFoo>> DisabledItems { get; } = GetDisabledItems();

private List<TreeViewItem<TreeFoo>> ExpandItems { get; set; } = GetExpandItems();
private List<TreeViewItem<TreeFoo>> ExpandItems { get; } = GetExpandItems();

private List<TreeViewItem<TreeFoo>> CheckedItems { get; set; } = GetCheckedItems();

private static List<TreeViewItem<TreeFoo>> GetIconItems() => TreeFoo.GetTreeItems();

private List<TreeViewItem<TreeFoo>> GetClickExpandItems { get; set; } = TreeFoo.GetTreeItems();
private List<TreeViewItem<TreeFoo>> GetClickExpandItems { get; } = TreeFoo.GetTreeItems();

private List<TreeViewItem<TreeFoo>> GetFormItems { get; set; } = TreeFoo.GetTreeItems();
private List<TreeViewItem<TreeFoo>> GetFormItems { get; } = TreeFoo.GetTreeItems();

private List<TreeViewItem<TreeFoo>> CheckedItems2 { get; set; } = TreeFoo.GetTreeItems();
private List<TreeViewItem<TreeFoo>> CheckedItems2 { get; } = TreeFoo.GetTreeItems();

private List<TreeViewItem<TreeFoo>> KeyboardItems { get; set; } = TreeFoo.GetTreeItems();
private List<TreeViewItem<TreeFoo>> KeyboardItems { get; } = TreeFoo.GetTreeItems();

private List<SelectedItem> SelectedItems { get; set; } = TreeFoo.GetItems().Select(x => new SelectedItem(x.Id, x.Text)).ToList();
private List<SelectedItem> SelectedItems { get; } = TreeFoo.GetItems().Select(x => new SelectedItem(x.Id, x.Text)).ToList();

private TreeView<TreeFoo>? SetActiveTreeView { get; set; }

private List<TreeViewItem<TreeFoo>>? AsyncItems { get; set; }

private List<TreeViewItem<TreeFoo>>? SearchItems { get; set; } = TreeFoo.GetTreeItems();

private List<TreeViewItem<TreeFoo>> VirtualizeItems { get; } = TreeFoo.GetVirtualizeTreeItems();

private Foo Model => Foo.Generate(LocalizerFoo);

private string? _selectedValue;
Expand Down Expand Up @@ -229,6 +231,18 @@ private Task OnSearchAsync(string searchText)
return Task.CompletedTask;
}

private static async Task<IEnumerable<TreeViewItem<TreeFoo>>> OnExpandVirtualNodeAsync(TreeViewItem<TreeFoo> node)
{
await Task.Delay(500);
var items = new List<TreeViewItem<TreeFoo>>();
Enumerable.Range(1, 1000).ToList().ForEach(i =>
{
var text = $"{node.Text}-{i}";
items.Add(new TreeViewItem<TreeFoo>(new TreeFoo() { Text = text }) { Text = text, HasChildren = Random.Shared.Next(100) > 80 });
});
return items;
}

private class CustomerTreeItem : ComponentBase
{
[Inject]
Expand Down Expand Up @@ -432,14 +446,6 @@ private static AttributeItem[] GetTreeItemAttributes() =>
DefaultValue = " false "
},
new()
{
Name = nameof(TreeViewItem<TreeFoo>.ShowLoading),
Description = "Whether to show child node loading animation",
Type = "bool",
ValueList = " true|false ",
DefaultValue = " false "
},
new()
{
Name = nameof(TreeViewItem<TreeFoo>.Template),
Description = "Child node template",
Expand Down
57 changes: 29 additions & 28 deletions src/BootstrapBlazor.Server/Data/TreeFoo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,44 +59,45 @@ public static List<TreeViewItem<TreeFoo>> GetTreeItems()
return CascadingTree(items);
}

public static List<TreeViewItem<TreeFoo>> GetVirtualizeTreeItems()
{
var ret = new List<TreeViewItem<TreeFoo>>();
Enumerable.Range(1, 100).ToList().ForEach(i =>
{
ret.Add(new TreeViewItem<TreeFoo>(new TreeFoo() { Id = $"{i}" })
{
Text = $"Root{i}",
HasChildren = true
});
});
return ret;
}

/// <summary>
/// TreeFoo 带选择框树状数据集
/// </summary>
/// <returns></returns>
public static List<TreeViewItem<TreeFoo>> GetCheckedTreeItems(string? parentId = null)
{
return [
new(new TreeFoo()
{
Id = $"{parentId}-101",
ParentId=parentId
})
{
Text = "navigation one",
HasChildren = true
},
new(new TreeFoo()
{
Id = $"{parentId}-102",
ParentId=parentId
})
{
Text = "navigation two",
CheckedState = CheckboxState.Checked
}
];
var node1 = new TreeViewItem<TreeFoo>(new TreeFoo() { Id = $"{parentId}-101", ParentId = parentId })
{
Text = "navigation one", HasChildren = true
};
var node2 = new TreeViewItem<TreeFoo>(new TreeFoo() { Id = $"{parentId}-102", ParentId = parentId })
{
Text = "navigation two", CheckedState = CheckboxState.Checked
};
return [node1, node2];
}

/// <summary>
/// 树状数据层次化方法
/// </summary>
/// <param name="items">数据集合</param>
public static List<TreeViewItem<TreeFoo>> CascadingTree(IEnumerable<TreeFoo> items) => items.CascadingTree(null,
(foo, parent) => foo.ParentId == parent?.Value.Id,
foo => new TreeViewItem<TreeFoo>(foo)
{
Text = foo.Text,
Icon = foo.Icon,
IsActive = foo.IsActive
});
public static List<TreeViewItem<TreeFoo>> CascadingTree(IEnumerable<TreeFoo> items) => items.CascadingTree(null, (foo, parent) => foo.ParentId == parent?.Value.Id, foo => new TreeViewItem<TreeFoo>(foo)
{
Text = foo.Text,
Icon = foo.Icon,
IsActive = foo.IsActive
});
}
88 changes: 50 additions & 38 deletions src/BootstrapBlazor/Components/TreeView/TreeView.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@namespace BootstrapBlazor.Components
@using Microsoft.AspNetCore.Components.Web.Virtualization
@namespace BootstrapBlazor.Components
@typeparam TItem
@inherits BootstrapModuleComponentBase
@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]
Expand Down Expand Up @@ -37,12 +38,25 @@ else
@SearchTemplate
}
}
<ul class="tree-root scroll" tabindex="0">
@foreach (var item in Items)
{
@RenderTreeItem(item)
}
</ul>
@if (IsVirtualize)
{
<div class="tree-root is-virtual scroll" tabindex="0">
<Virtualize ItemSize="RowHeight" OverscanCount="10" Items="@Rows">
<ItemContent>
@RenderTreeRow(context)
</ItemContent>
</Virtualize>
</div>
}
else
{
<ul class="tree-root scroll" tabindex="0">
@foreach (var item in Items)
{
@RenderTreeItem(item)
}
</ul>
}
</div>
}

Expand All @@ -57,39 +71,37 @@ else

private RenderFragment<TreeViewItem<TItem>> RenderTreeItem => item =>
@<li class="@GetItemClassString(item)">
<div class="tree-content" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd">
<DynamicElement TagName="i" class="@GetCaretClassString(item)" TriggerClick="TriggerNodeArrow(item)" OnClick="() => OnToggleNodeAsync(item, true)"></DynamicElement>
@if (ShowCheckbox)
{
<Checkbox Value="@item.CheckedState" IsDisabled="GetItemDisabledState(item)" SkipValidate="true"
ShowLabel="false" ShowAfterLabel="false" @bind-State="@item.CheckedState"
OnBeforeStateChanged="@(MaxSelectedCount > 0 ? state => OnBeforeStateChangedCallback(item, state) : null)"
OnStateChanged="(state, v) => OnCheckStateChanged(item, true)" StopPropagation="true" />
}
<DynamicElement class="@GetNodeClassString(item)" TriggerClick="TriggerNodeLabel(item)" OnClick="() => OnClick(item)">
@if (ShowIcon)
{
<i class="@GetIconClassString(item)"></i>
}
@if (item.Template == null)
{
<span class="@item.CssClass">@item.Text</span>
}
else
{
@item.Template(item.Value)
}
</DynamicElement>
</div>
@if (item.ShowLoading)
{
<ul class="tree-ul show">
<Spinner Size="Size.Small" Color="Color.Primary" />
</ul>
}
else if (item.Items.Any())
@RenderTreeRow(item)
@if (item.Items.Any())
{
@RenderTreeNode(item)
}
</li>;

private RenderFragment<TreeViewItem<TItem>> RenderTreeRow => item =>
@<div @key="item" class="tree-content" data-bb-tree-view-index="@Rows.IndexOf(item)" @oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault" @ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd" style="@GetTreeRowStyle(item)">
<DynamicElement TagName="i" class="@GetCaretClassString(item)" TriggerClick="TriggerNodeArrow(item)" OnClick="() => OnToggleNodeAsync(item, true)"></DynamicElement>
<i class="@NodeLoadingClassString"></i>
@if (ShowCheckbox)
{
<Checkbox Value="@item" IsDisabled="GetItemDisabledState(item)"
SkipValidate="true" ShowLabel="false" ShowAfterLabel="false"
State="@item.CheckedState" OnStateChanged="(state, v) => OnCheckStateChanged(item, state)"
OnBeforeStateChanged="@(MaxSelectedCount > 0 ? state => OnBeforeStateChangedCallback(item, state) : null)" ></Checkbox>
}
<DynamicElement class="@GetNodeClassString(item)" TriggerClick="TriggerNodeLabel(item)" OnClick="() => OnClick(item)">
@if (ShowIcon)
{
<i class="@GetIconClassString(item)"></i>
}
@if (item.Template == null)
{
<span class="@GetItemTextClassString(item)">@item.Text</span>
}
else
{
@item.Template(item.Value)
}
</DynamicElement>
</div>;
}
Loading

0 comments on commit 17ed9ac

Please sign in to comment.