diff --git a/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor new file mode 100644 index 00000000000..072653fd5a0 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor @@ -0,0 +1,79 @@ +@page "/select-table" + +

TableSelect 表格选择器

+ +

下拉框为表格用于展示复杂类型的选择需求

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.cs new file mode 100644 index 00000000000..62743adb8b1 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.cs @@ -0,0 +1,135 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// 可选择表格组件示例 +/// +public partial class SelectTables +{ + [Inject] + [NotNull] + private IStringLocalizer? LocalizerFoo { get; set; } + + [Inject] + [NotNull] + private IStringLocalizer? Localizer { get; set; } + + private List _items = default!; + + private List _colorItems = default!; + + private List _templateItems = default!; + + private List _disabledItems = default!; + + private List _validateFormItems = default!; + + private Foo? _foo; + + private Foo? _colorFoo; + + private Foo? _templateFoo; + + private Foo? _disabledFoo; + + private Foo? _validateFormFoo; + + private Foo Model = new(); + + /// + /// + /// + protected override void OnInitialized() + { + _items = Foo.GenerateFoo(LocalizerFoo); + _colorItems = Foo.GenerateFoo(LocalizerFoo); + _templateItems = Foo.GenerateFoo(LocalizerFoo); + _disabledItems = Foo.GenerateFoo(LocalizerFoo); + _validateFormItems = Foo.GenerateFoo(LocalizerFoo); + } + + private static string? GetTextCallback(Foo? foo) => foo?.Name; + + /// + /// 获得属性方法 + /// + /// + private AttributeItem[] GetAttributes() => + [ + new() + { + Name = "Items", + Description = Localizer["AttributeItems"], + Type = "IEnumerable", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "TableColumns", + Description = Localizer["AttributeTableColumns"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Color", + Description = Localizer["AttributeColor"], + Type = "Color", + ValueList = "Primary / Secondary / Success / Danger / Warning / Info / Dark", + DefaultValue = "Primary" + }, + new() + { + Name = "IsDisabled", + Description = Localizer["AttributeIsDisabled"], + Type = "boolean", + ValueList = "true / false", + DefaultValue = "false" + }, + new() + { + Name = "ShowAppendArrow", + Description = Localizer["AttributeShowAppendArrow"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "true" + }, + new() + { + Name = "GetTextCallback", + Description = Localizer["AttributeGetTextCallback"], + Type = "Func", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "PlaceHolder", + Description = Localizer["AttributePlaceHolder"], + Type = "string?", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Height", + Description = Localizer["AttributeHeight"], + Type = "int", + ValueList = " — ", + DefaultValue = "486" + }, + new() + { + Name = "Template", + Description = Localizer["AttributeTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + } + ]; +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.css b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.css new file mode 100644 index 00000000000..317023e3bdb --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/SelectTables.razor.css @@ -0,0 +1,56 @@ +::deep .divider { + --bb-divider-margin: 1rem 0; + --bb-divider-bg: #c0c4cc; +} + +::deep .dropdown-item-demo { + border-radius: var(--bs-border-radius); + border: var(--bs-border-width) solid var(--bs-border-color); + padding: .5rem; + flex-direction: column; +} + +::deep .select-custom-header { + display: flex; + align-items: center; + margin-top: .5rem; +} + + ::deep .select-custom-header .id { + background-color: var(--bs-success); + padding: .25rem .5rem; + border-radius: var(--bs-border-radius); + } + + ::deep .select-custom-header .name { + padding: .25rem .5rem; + margin: 0 1rem; + flex: 1; + font-weight: bold; + } + + ::deep .select-custom-header .status { + } + +::deep .select-custom-body { + display: flex; +} + + ::deep .select-custom-body .progress { + height: 6px; + margin-bottom: .5rem; + } + + ::deep .select-custom-body .bb-avatar { + width: 102px; + border: 2px solid var(--bs-info); + } + +::deep .select-custom-detail { + flex: 1; + margin-left: 2rem; +} + + ::deep .select-custom-detail > div { + margin-bottom: .5rem; + } diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index 132516a8424..2c97aeb2712 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -406,6 +406,12 @@ void AddForm(DemoMenuItem item) Url = "select" }, new() + { + IsNew = true, + Text = Localizer["SelectTable"], + Url = "select-table" + }, + new() { Text = Localizer["SelectTree"], Url = "select-tree" diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 7c24c0ed6fe..82f803c32c6 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -4407,6 +4407,7 @@ "Radio": "Radio", "Rate": "Rate", "Select": "Select", + "SelectTable": "Select-Table", "SelectTree": "Select-Tree", "Slider": "Slider", "Switch": "Switch", @@ -6139,5 +6140,26 @@ "Show": "Show", "ToolboxCardTitle": "Tools", "Translate": "Translate" + }, + "BootstrapBlazor.Server.Components.Samples.SelectTables": { + "NormalTitle": "Basic usage", + "NormalIntro": "Suitable for candidates with a relatively large amount of information, presenting information using Table", + "ColorTitle": "Color", + "ColorIntro": "Change component border color by setting Color", + "IsDisabledTitle": "Disabled", + "IsDisabledIntro": "When you set the IsDisabled property value to true, the component suppresses select", + "TemplateTitle": "Value Template", + "TemplateIntro": "Present customized display content by customizing the Template", + "AttributeItems": "Data source table display content set", + "AttributeTableColumns": "Table Display Column Set", + "AttributeColor": "Color", + "AttributeIsDisabled": "Is it disabled", + "AttributeShowAppendArrow": "Is the right extended small arrow displayed", + "AttributeGetTextCallback": "Get display value callback method", + "AttributePlaceHolder": "Placeholder", + "AttributeHeight": "Table height", + "AttributeTemplate": "Display Template", + "ValidateFormTitle": "ValidateForm", + "ValidateFormIntro": "Intercept data when the value is empty" } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 6eae294fc9b..03205895d4c 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -4407,6 +4407,7 @@ "Radio": "单选框 Radio", "Rate": "评分 Rate", "Select": "选择器 Select", + "SelectTable": "表格选择器 SelectTable", "SelectTree": "树状选择器 SelectTree", "Slider": "滑块 Slider", "Switch": "开关 Switch", @@ -6139,5 +6140,26 @@ "Show": "显示", "ToolboxCardTitle": "工具栏", "Translate": "机翻" + }, + "BootstrapBlazor.Server.Components.Samples.SelectTables": { + "NormalTitle": "基本功能", + "NormalIntro": "适用于候选项信息量比较大,用 Table 呈现信息量", + "ColorTitle": "颜色", + "ColorIntro": "通过设置 Color 改变组件边框颜色", + "IsDisabledTitle": "禁用", + "IsDisabledIntro": "设置 IsDisabled 属性值为 true 时,组件禁止选择", + "TemplateTitle": "显示模板", + "TemplateIntro": "通过自定义 Template 模板,呈现定制化显示内容", + "AttributeItems": "数据源表格显示内容集合", + "AttributeTableColumns": "设置表格显示列集合", + "AttributeColor": "颜色", + "AttributeIsDisabled": "是否禁用", + "AttributeShowAppendArrow": "是否显示右侧扩展小箭头", + "AttributeGetTextCallback": "获得显示值回调方法", + "AttributePlaceHolder": "占位符", + "AttributeHeight": "表格高度", + "AttributeTemplate": "显示模板", + "ValidateFormTitle": "客户端验证", + "ValidateFormIntro": "下拉框未选择时,点击提交按钮时拦截。" } } diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index 929b62525f4..e6211dd5513 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -125,6 +125,7 @@ "search": "Searches", "select": "Selects", "select-tree": "SelectTrees", + "select-table": "SelectTables", "segmented": "Segmenteds", "slider": "Sliders", "slide-button": "SlideButtons", diff --git a/src/BootstrapBlazor/Components/Select/SelectTable.razor b/src/BootstrapBlazor/Components/Select/SelectTable.razor new file mode 100644 index 00000000000..442d31649f2 --- /dev/null +++ b/src/BootstrapBlazor/Components/Select/SelectTable.razor @@ -0,0 +1,42 @@ +@namespace BootstrapBlazor.Components +@typeparam TItem +@inherits PopoverDropdownBase +@attribute [BootstrapModuleAutoLoader("Select/SelectTable.razor.js", JSObjectReference = true)] + +@if (IsShowLabel) +{ + +} +
+ + + @TableColumns?.Invoke(new TItem()) + + + + +
diff --git a/src/BootstrapBlazor/Components/Select/SelectTable.razor.cs b/src/BootstrapBlazor/Components/Select/SelectTable.razor.cs new file mode 100644 index 00000000000..2d4c3b3481d --- /dev/null +++ b/src/BootstrapBlazor/Components/Select/SelectTable.razor.cs @@ -0,0 +1,183 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.Localization; + +namespace BootstrapBlazor.Components; + +/// +/// Select 组件实现类 +/// +/// +[CascadingTypeParameter(nameof(TItem))] +public partial class SelectTable : ITable where TItem : class, new() +{ + /// + /// 获得/设置 TableHeader 实例 + /// + [Parameter] + public RenderFragment? TableColumns { get; set; } + + /// + /// 获得/设置 绑定数据集 + /// + [Parameter] + [NotNull] + public IEnumerable? Items { get; set; } + + /// + /// 获得/设置 颜色 默认 Color.None 无设置 + /// + [Parameter] + public Color Color { get; set; } + + /// + /// 获得/设置 是否显示组件右侧扩展箭头 默认 true 显示 + /// + [Parameter] + public bool ShowAppendArrow { get; set; } = true; + + /// + /// 获得 显示文字回调方法 默认 null + /// + [Parameter] + public Func? GetTextCallback { get; set; } + + /// + /// 获得/设置 右侧下拉箭头图标 默认 fa-solid fa-angle-up + /// + [Parameter] + [NotNull] + public string? DropdownIcon { get; set; } + + /// + /// 获得/设置 IIconTheme 服务实例 + /// + [Inject] + [NotNull] + protected IIconTheme? IconTheme { get; set; } + + /// + /// 获得表格列集合 + /// + public List Columns { get; } = []; + + List ITable.Columns { get => Columns; } + + [ExcludeFromCodeCoverage] + Dictionary ITable.Filters { get; } = []; + + [ExcludeFromCodeCoverage] + Func? ITable.OnFilterAsync { get => null; } + + [ExcludeFromCodeCoverage] + IEnumerable ITable.GetVisibleColumns() => Columns; + + /// + /// 获得 样式集合 + /// + private string? ClassName => CssBuilder.Default("select select-table dropdown") + .AddClass("disabled", IsDisabled) + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + /// + /// 获得 样式集合 + /// + private string? InputClassName => CssBuilder.Default("form-select form-control") + .AddClass($"border-{Color.ToDescriptionString()}", Color != Color.None && !IsDisabled && !IsValid.HasValue) + .AddClass($"border-success", IsValid.HasValue && IsValid.Value) + .AddClass($"border-danger", IsValid.HasValue && !IsValid.Value) + .AddClass(FieldClass, IsNeedValidate) + .AddClass(ValidCss) + .Build(); + + /// + /// 获得 样式集合 + /// + private string? AppendClassString => CssBuilder.Default("form-select-append") + .AddClass($"text-{Color.ToDescriptionString()}", Color != Color.None && !IsDisabled && !IsValid.HasValue) + .AddClass($"text-success", IsValid.HasValue && IsValid.Value) + .AddClass($"text-danger", IsValid.HasValue && !IsValid.Value) + .Build(); + + /// + /// 获得 PlaceHolder 属性 + /// + [Parameter] + public string? PlaceHolder { get; set; } + + /// + /// 获得/设置 表格高度 + /// + [Parameter] + public int Height { get; set; } = 486; + + /// + /// 获得/设置 Value 显示模板 默认 null + /// + /// 默认通过 + [Parameter] + public RenderFragment? Template { get; set; } + + [Inject] + [NotNull] + private IStringLocalizer>? Localizer { get; set; } + + /// + /// 获得 input 组件 Id 方法 + /// + /// + protected override string? RetrieveId() => InputId; + + /// + /// 获得/设置 内部 Input 组件 Id + /// + private string InputId => $"{Id}_input"; + + private string GetStyleString => $"height: {Height}px;"; + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + if (ValidateForm != null) + { + Rules.Add(new RequiredValidator()); + } + } + + /// + /// OnParametersSet 方法 + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + Items ??= []; + PlaceHolder ??= Localizer[nameof(PlaceHolder)]; + DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectDropdownIcon); + } + + /// + /// + /// + /// + protected override bool IsRequired() => ValidateForm != null; + + /// + /// 获得 Text 显示文字 + /// + /// + private string? GetText() => Value == default ? null : GetTextCallback?.Invoke(Value) ?? Value.ToString(); + + private async Task OnClickRowCallback(TItem item) + { + CurrentValue = item; + await InvokeVoidAsync("close", Id); + } +} diff --git a/src/BootstrapBlazor/Components/Select/SelectTable.razor.js b/src/BootstrapBlazor/Components/Select/SelectTable.razor.js new file mode 100644 index 00000000000..b3b99a3e982 --- /dev/null +++ b/src/BootstrapBlazor/Components/Select/SelectTable.razor.js @@ -0,0 +1,54 @@ +import { getWidth } from "../../modules/utility.js?v=$version" +import Data from "../../modules/data.js?v=$version" +import Popover from "../../modules/base-popover.js?v=$version" + +export function init(id) { + const el = document.getElementById(id) + if (el == null) { + return + } + + const popover = Popover.init(el, { + initCallback: () => { + const width = getWidth(el); + const dropdown = el.querySelector('.dropdown-table'); + if (dropdown) { + dropdown.style.setProperty('--bb-dropdown-table-width', `${width}px`); + + dropdown.style.setProperty('position', 'fixed'); + dropdown.style.setProperty('visibility', 'hidden'); + dropdown.style.setProperty('display', 'block'); + + const wrapper = dropdown.querySelector('.table-wrapper'); + const headerHeight = wrapper.children[0].offsetHeight; + wrapper.children[1].style.setProperty('height', `calc(100% - ${headerHeight}px)`); + + dropdown.style.removeProperty('display'); + dropdown.style.removeProperty('visibility'); + dropdown.style.removeProperty('position'); + } + } + }); + const selectTable = { + el, + input: el.querySelector(".form-select"), + popover + } + + Data.set(id, selectTable) +} + +export function close(id) { + const data = Data.get(id) + if (data) { + data.popover.popover.hide(); + } +} +export function dispose(id) { + const data = Data.get(id) + Data.remove(id) + + if (data) { + Popover.dispose(data.popover) + } +} diff --git a/src/BootstrapBlazor/Components/Select/SelectTable.razor.scss b/src/BootstrapBlazor/Components/Select/SelectTable.razor.scss new file mode 100644 index 00000000000..144acfb519b --- /dev/null +++ b/src/BootstrapBlazor/Components/Select/SelectTable.razor.scss @@ -0,0 +1,14 @@ +.select-table { + .dropdown-toggle { + .is-display { + cursor: pointer; + } + } + + &.disabled { + .is-display { + background-color: var(--bs-secondary-bg); + cursor: not-allowed; + } + } +} diff --git a/src/BootstrapBlazor/Components/Table/ITable.cs b/src/BootstrapBlazor/Components/Table/ITable.cs index da8906ecd37..2e682d3fe98 100644 --- a/src/BootstrapBlazor/Components/Table/ITable.cs +++ b/src/BootstrapBlazor/Components/Table/ITable.cs @@ -27,5 +27,5 @@ public interface ITable /// /// 获得 过滤异步回调方法 /// - Func OnFilterAsync { get; } + Func? OnFilterAsync { get; } } diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index fd9a6296895..8c0e583be75 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -205,7 +205,11 @@ protected virtual bool TryParseValueFromString(string value, [MaybeNullWhen(fals /// protected virtual string? FormatParsingErrorMessage() => ParsingErrorMessage; - private bool IsRequired() => FieldIdentifier + /// + /// 判断是否为必填字段 + /// + /// + protected virtual bool IsRequired() => FieldIdentifier ?.Model.GetType().GetPropertyByName(FieldIdentifier.Value.FieldName)!.GetCustomAttribute(true) != null || (ValidateRules?.OfType().Select(i => i.Validator).OfType().Any() ?? false); @@ -213,7 +217,7 @@ private bool IsRequired() => FieldIdentifier /// Gets a string that indicates the status of the field being edited. This will include /// some combination of "modified", "valid", or "invalid", depending on the status of the field. /// - private string FieldClass => (EditContext != null && FieldIdentifier != null) ? EditContext.FieldCssClass(FieldIdentifier.Value) : ""; + protected string FieldClass => (EditContext != null && FieldIdentifier != null) ? EditContext.FieldCssClass(FieldIdentifier.Value) : ""; /// /// Gets a CSS class string that combines the class attribute and diff --git a/src/BootstrapBlazor/wwwroot/modules/base-popover.js b/src/BootstrapBlazor/wwwroot/modules/base-popover.js index 3389ac7998a..0d3a47baa63 100644 --- a/src/BootstrapBlazor/wwwroot/modules/base-popover.js +++ b/src/BootstrapBlazor/wwwroot/modules/base-popover.js @@ -11,7 +11,8 @@ const Popover = { dropdownSelector: '.dropdown-menu', isDisabled: () => { return isDisabled(el) || isDisabled(el.parentNode) || isDisabled(el.querySelector('.form-control')) - } + }, + initCallback: null }, ...config || {} } @@ -99,6 +100,9 @@ const Popover = { if (!popover.popover) { popover.popover = new bootstrap.Popover(popover.toggleElement) hackPopover(popover.popover, popover.class) + if (popover.initCallback) { + popover.initCallback(); + } popover.popover.toggle() } } diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss index 1536dc2f413..66db2d13489 100644 --- a/src/BootstrapBlazor/wwwroot/scss/components.scss +++ b/src/BootstrapBlazor/wwwroot/scss/components.scss @@ -71,6 +71,7 @@ @import "../../Components/Scroll/Scroll.razor.scss"; @import "../../Components/Select/MultiSelect.razor.scss"; @import "../../Components/Select/Select.razor.scss"; +@import "../../Components/Select/SelectTable.razor.scss"; @import "../../Components/Select/SelectTree.razor.scss"; @import "../../Components/Segmented/Segmented.razor.scss"; @import "../../Components/Skeleton/Skeleton.razor.scss"; diff --git a/src/BootstrapBlazor/wwwroot/scss/root.scss b/src/BootstrapBlazor/wwwroot/scss/root.scss index 78cbe41a866..0fda8dd1c41 100644 --- a/src/BootstrapBlazor/wwwroot/scss/root.scss +++ b/src/BootstrapBlazor/wwwroot/scss/root.scss @@ -70,3 +70,17 @@ a, a:hover, a:focus { @include direction(right,last); @include direction(left,first) } + +.popover-dropdown { + .dropdown-table { + --bs-dropdown-padding-y: 0; + --bb-dropdown-max-height: 468px; + --bb-dropdown-table-width: auto; + overflow: hidden; + width: var(--bb-dropdown-table-width); + + .table-wrapper { + border: none; + } + } +} diff --git a/test/UnitTest/Components/SelectTableTest.cs b/test/UnitTest/Components/SelectTableTest.cs new file mode 100644 index 00000000000..1a4f339d896 --- /dev/null +++ b/test/UnitTest/Components/SelectTableTest.cs @@ -0,0 +1,273 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; + +namespace UnitTest.Components; + +public class SelectTableTest : BootstrapBlazorTestBase +{ + [Fact] + public void Items_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(); + }); + var rows = cut.FindAll("tbody > tr"); + Assert.Empty(rows); + } + + [Fact] + public void Color_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Color, Color.Danger); + }); + }); + cut.Contains("border-danger"); + } + + [Fact] + public void ShowAppendArrow_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.ShowAppendArrow, false); + }); + }); + cut.DoesNotContain("form-select-append"); + } + + [Fact] + public void Template_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 4); + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Address"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Address", typeof(string))); + builder.CloseComponent(); + }); + pb.Add(a => a.Template, foo => builder => + { + builder.AddContent(0, $"Template-{foo.Name}"); + }); + }); + }); + var rows = cut.FindAll("tbody > tr"); + Assert.Equal(4, rows.Count); + + var table = cut.FindComponent>(); + table.SetParametersAndRender(pb => + { + pb.Add(a => a.Value, items[0]); + }); + Assert.Contains($"Template-{items[0].Name}", cut.Markup); + } + + [Fact] + public void GetTextCallback_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 4); + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.Value, items[0]); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Address"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Address", typeof(string))); + builder.CloseComponent(); + }); + pb.Add(a => a.GetTextCallback, foo => foo.Name); + }); + }); + Assert.Contains($"value=\"{items[0].Name}\"", cut.Markup); + + var table = cut.FindComponent>(); + table.SetParametersAndRender(pb => + { + pb.Add(a => a.GetTextCallback, foo => null); + }); + Assert.Contains("value=\"BootstrapBlazor.Server.Data.Foo\"", cut.Markup); + + table.SetParametersAndRender(pb => + { + pb.Add(a => a.GetTextCallback, null); + }); + Assert.Contains("value=\"BootstrapBlazor.Server.Data.Foo\"", cut.Markup); + + table.SetParametersAndRender(pb => + { + pb.Add(a => a.Value, null); + }); + Assert.DoesNotContain("value=\"\"", cut.Markup); + } + + [Fact] + public void Height_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 4); + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.Value, items[0]); + pb.Add(a => a.Height, 100); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Address"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Address", typeof(string))); + builder.CloseComponent(); + }); + }); + }); + Assert.Contains($"height: 100px;", cut.Markup); + } + + [Fact] + public async Task Value_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 4); + Foo? v = null; + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.Value, items[0]); + pb.Add(a => a.OnValueChanged, foo => + { + v = foo; + return Task.CompletedTask; + }); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Address"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Address", typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + var rows = cut.FindAll("tbody > tr"); + await cut.InvokeAsync(() => + { + rows[1].Click(); + }); + Assert.Equal(items[1].Name, v?.Name); + } + + [Fact] + public async Task Validate_Ok() + { + var localizer = Context.Services.GetRequiredService>(); + var items = Foo.GenerateFoo(localizer, 4); + var valid = false; + var invalid = false; + var model = new SelectTableModel() { Foo = items[0] }; + var cut = Context.RenderComponent(builder => + { + builder.Add(a => a.OnValidSubmit, context => + { + valid = true; + return Task.CompletedTask; + }); + builder.Add(a => a.OnInvalidSubmit, context => + { + invalid = true; + return Task.CompletedTask; + }); + builder.Add(a => a.Model, model); + builder.AddChildContent>(pb => + { + pb.Add(a => a.Value, model.Foo); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(model, "Foo", typeof(Foo))); + pb.Add(a => a.OnValueChanged, v => + { + model.Foo = v; + return Task.CompletedTask; + }); + pb.Add(a => a.Items, items); + pb.Add(a => a.TableColumns, foo => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Name"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string))); + builder.CloseComponent(); + + builder.OpenComponent>(0); + builder.AddAttribute(1, "Field", "Address"); + builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Address", typeof(string))); + builder.CloseComponent(); + }); + }); + }); + + await cut.InvokeAsync(() => + { + var form = cut.Find("form"); + form.Submit(); + }); + Assert.True(valid); + + model.Foo = null; + var table = cut.FindComponent>(); + table.SetParametersAndRender(); + await cut.InvokeAsync(() => + { + var form = cut.Find("form"); + form.Submit(); + }); + Assert.True(invalid); + } + + class SelectTableModel() + { + public Foo? Foo { get; set; } + } +} diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index a9522b1d4a2..3304a300d12 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -19,8 +19,8 @@ public void OnSearchTextChanged_Null() pb.Add(a => a.ShowSearch, true); pb.Add(a => a.Items, new List() { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") { IsDisabled = true } + new("1", "Test1"), + new("2", "Test2") { IsDisabled = true } }); }); }); @@ -92,9 +92,9 @@ public void IsClearable_Ok() pb.Add(a => a.IsClearable, true); pb.Add(a => a.Items, new List() { - new SelectedItem("", "请选择"), - new SelectedItem("2", "Test2"), - new SelectedItem("3", "Test3") + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") }); pb.Add(a => a.Value, ""); pb.Add(a => a.OnValueChanged, v => @@ -169,8 +169,8 @@ public void OnSelectedItemChanged_OK() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("", "Test"), - new SelectedItem("1", "Test2") + new("", "Test"), + new("1", "Test2") }); pb.Add(a => a.Value, ""); pb.Add(a => a.OnSelectedItemChanged, item => @@ -209,9 +209,9 @@ public void OnSelectedItemChanged_OK() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("", "Test"), - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("", "Test"), + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); }); @@ -240,8 +240,8 @@ public void DisableItemChangedWhenFirstRender_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test"), - new SelectedItem("2", "Test2") + new("1", "Test"), + new("2", "Test2") }); pb.Add(a => a.Value, ""); pb.Add(a => a.OnSelectedItemChanged, item => @@ -294,9 +294,9 @@ public void Validate_Ok() pb.Add(a => a.ValueExpression, model.GenerateValueExpression()); pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("", "Test"), - new SelectedItem("1", "Test1") { GroupName = "Test1" }, - new SelectedItem("2", "Test2") { GroupName = "Test2" } + new("", "Test"), + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } }); }); }); @@ -325,8 +325,8 @@ public void ItemTemplate_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1") { GroupName = "Test1" }, - new SelectedItem("2", "Test2") { GroupName = "Test2" } + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } }); pb.Add(a => a.Value, "2"); pb.Add(a => a.ItemTemplate, item => builder => @@ -347,8 +347,8 @@ public void GroupItemTemplate_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1") { GroupName = "Test1" }, - new SelectedItem("2", "Test2") { GroupName = "Test2" } + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } }); pb.Add(a => a.Value, "2"); pb.Add(a => a.GroupItemTemplate, title => builder => @@ -377,8 +377,8 @@ public void NullBool_Ok() { pb.Add(a => a.Items, new List { - new SelectedItem("true", "True"), - new SelectedItem("false", "False"), + new("true", "True"), + new("false", "False"), }); pb.Add(a => a.Value, null); }); @@ -396,8 +396,8 @@ public void SelectItem_Ok() { pb.Add(a => a.Items, new List { - new SelectedItem("1", "Text1"), - new SelectedItem("2", "Text2"), + new("1", "Text1"), + new("2", "Text2"), }); pb.Add(a => a.Value, v); pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, i => v = i)); @@ -415,8 +415,8 @@ public void SearchIcon_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.ShowSearch, true); @@ -432,8 +432,8 @@ public void IsFixedSearch_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.ShowSearch, true); @@ -450,8 +450,8 @@ public void CustomClass_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.CustomClass, "test-custom-class"); @@ -466,8 +466,8 @@ public void ShowShadow_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); }); @@ -487,8 +487,8 @@ public void DropdownIcon_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.DropdownIcon, "search-icon"); @@ -503,8 +503,8 @@ public void DisplayTemplate_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.DisplayTemplate, item => builder => @@ -522,8 +522,8 @@ public void IsPopover_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.IsPopover, true); @@ -539,8 +539,8 @@ public void Offset_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.IsPopover, false); @@ -556,8 +556,8 @@ public void Placement_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.Placement, Placement.Top); @@ -578,8 +578,8 @@ public void ItemClick_Ok() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.IsPopover, true); @@ -600,8 +600,8 @@ public void IsVirtualize_Items() { pb.Add(a => a.Items, new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }); pb.Add(a => a.Value, "2"); pb.Add(a => a.IsVirtualize, true); @@ -635,8 +635,8 @@ public void IsVirtualize_OnQueryAsync() { Items = new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }, TotalCount = 2 }); @@ -674,8 +674,8 @@ public void IsVirtualize_BindValue() { Items = new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }, TotalCount = 2 }); @@ -721,8 +721,8 @@ public void IsVirtualize_DefaultVirtualizeItemText() { Items = new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }, TotalCount = 2 }); @@ -762,8 +762,8 @@ public void TryParseValueFromString_Ok() { var items = new SelectedItem[] { - new SelectedItem("1", "Test1"), - new SelectedItem("2", "Test2") + new("1", "Test1"), + new("2", "Test2") }; var cut = Context.RenderComponent>(pb => {