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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@
</MudSelectItem>
</MudSelect>

<MudSelect @bind-Value="country" Label="ToStringFunc Precedence" Variant="Variant.Outlined" ToStringFunc="@GetCountryCode">
<MudSelectItem Value="@("Austria")">
<img src="https://upload.wikimedia.org/wikipedia/commons/4/41/Flag_of_Austria.svg" height="14" class="mr-1" /> Austria
</MudSelectItem>
<MudSelectItem Value="@("Hungary")">
<img src="https://upload.wikimedia.org/wikipedia/commons/0/00/Flag_of_Hungary.png" height="14" class="mr-1" /> Hungary
</MudSelectItem>
<MudSelectItem Value="@("Sweden")">
<img src="https://upload.wikimedia.org/wikipedia/commons/4/4b/Flag_of_Sweden_fixed.svg" height="14" class="mr-1" /> Sweden
</MudSelectItem>
</MudSelect>

@code {
string country="Austria";
}

private string GetCountryCode(string name) => name switch
{
"Austria" => "AUT",
"Hungary" => "HUN",
"Sweden" => "SWE",
_ => name
};
}
15 changes: 13 additions & 2 deletions src/MudBlazor.Docs/Pages/Components/Select/SelectPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,19 @@
<DocsPageSection>
<SectionHeader Title="Value presentation">
<Description>
Select uses the render fragments you specify for the items to display the selected value (if any). If you don't specify render fragments, the value will be displayed as text. Also, if you have render fragments
but the value that has been set is not in the list, it will also be displayed as text.
Both the <CodeInline>ChildContent</CodeInline> (Render Fragment) and <CodeInline>ToStringFunc</CodeInline> can control how values are displayed.
<MudBadge Class="ml-4" Color="Color.Primary" Origin="Origin.CenterLeft" Dot Bordered>
<div class="pl-2">
When used on their own, either one determines how items appear in the dropdown list and how the selected value is displayed in the <CodeInline>MudSelect</CodeInline> input.
</div>
</MudBadge>
<MudBadge Class="ml-4" Color="Color.Primary" Origin="Origin.CenterLeft" Dot Bordered>
<div class="pl-2">
When both are used, <CodeInline>ChildContent</CodeInline> controls how items are rendered in the dropdown list, while <CodeInline>ToStringFunc</CodeInline> controls how the selected value is displayed.
</div>
</MudBadge>
<br /><br />
If neither is specified, the value’s default string representation is used.
</Description>
</SectionHeader>
<SectionContent ShowCode="false" Code="@nameof(SelectPresentationExample)">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@namespace MudBlazor.UnitTests.TestComponents.Select

<MudSelect T="string" Label="Select" ToStringFunc="@(x => x == "item1" ? null : x?.ToUpperInvariant())" Value="@("item1")">
<MudSelectItem Value="@("item1")">
<span class="custom-render">Item 1 Rendered</span>
</MudSelectItem>
<MudSelectItem Value="@("item2")">
<span class="custom-render">Item 2 Rendered</span>
</MudSelectItem>
</MudSelect>
62 changes: 62 additions & 0 deletions src/MudBlazor.UnitTests/Components/SelectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,5 +1891,67 @@ public void PopoverSettings_UsesGlobalDefaultsFromPopoverOptions()
// Modal should be null (using PopoverOptions defaults)
select.Instance.Modal.Should().BeNull();
}

[Test]
public async Task Select_ToStringFunc_ShouldTakePrecedenceOverChildContent()
{
var comp = Context.Render<SelectPrecedenceTest>();
var selectComponent = comp.FindComponent<MudSelect<string>>();
var select = selectComponent.Instance;

// 1. Initially item1 is selected. ToStringFunc returns null for item1.
// Should fall back to RenderFragment.
var displaySlots = comp.FindAll("div.mud-input-slot");
var displaySlot = displaySlots.FirstOrDefault(x => x.GetAttribute("style")?.Contains("display:inline") == true || x.GetAttribute("style")?.Contains("display: inline") == true);
displaySlot.Should().NotBeNull("initially it should fall back to RenderFragment");
displaySlot.InnerHtml.Should().Contain("custom-render");
displaySlot.TextContent.Trim().Should().Be("Item 1 Rendered");

// 2. Select item2. ToStringFunc returns "ITEM2" (not null).
// Should use ToStringFunc and NOT RenderFragment.
await comp.InvokeAsync(() => select!.SelectOption("item2"));
comp.Render();

displaySlots = comp.FindAll("div.mud-input-slot");
displaySlot = displaySlots.FirstOrDefault(x => x.GetAttribute("style")?.Contains("display:inline") == true || x.GetAttribute("style")?.Contains("display: inline") == true);
displaySlot.Should().BeNull("because ToStringFunc should take precedence over RenderFragment when it returns a non-null value");

var input = comp.Find("input");
input.GetAttribute("value").Should().Be("ITEM2");
}

[Test]
public async Task Select_FitContent_ShouldPrioritizeToStringFunc()
{
var comp = Context.Render<SelectPrecedenceTest>();
var selectComponent = comp.FindComponent<MudSelect<string>>();

// Remove label to avoid it being the longest
await selectComponent.SetParametersAndRenderAsync(parameters => parameters
.Add(x => x.FitContent, true)
.Add(x => x.Label, null)
.Add(x => x.ToStringFunc, new Func<string?, string?>(x => x == "item2" ? "VERY LONG ITEM 2" : null)));

// item1 -> null -> "Item 1 Rendered" (15 chars)
// item2 -> "VERY LONG ITEM 2" (16 chars)

// item2 is longest. ToStringFunc is NOT null for item2.
// filler should use "VERY LONG ITEM 2" and NOT RenderFragment.

var filler = comp.Find(".mud-select-filler");
filler.TextContent.Should().Contain("VERY LONG ITEM 2");
filler.InnerHtml.Should().NotContain("custom-render");

// Now make item1 longest via ToStringFunc
await selectComponent.SetParametersAndRenderAsync(parameters => parameters.Add(x => x.ToStringFunc, new Func<string?, string?>(x => x == "item1" ? "EXTREMELY LONG ITEM 1" : "ITEM 2")));

// Trigger recalculation of _longestItem by toggling FitContent
await selectComponent.SetParametersAndRenderAsync(parameters => parameters.Add(x => x.FitContent, false));
await selectComponent.SetParametersAndRenderAsync(parameters => parameters.Add(x => x.FitContent, true));

filler = comp.Find(".mud-select-filler");
filler.TextContent.Should().Contain("EXTREMELY LONG ITEM 1");
filler.InnerHtml.Should().NotContain("custom-render");
}
}
}
33 changes: 24 additions & 9 deletions src/MudBlazor/Components/Select/MudSelect.razor
Original file line number Diff line number Diff line change
Expand Up @@ -61,29 +61,44 @@
{
<MudStack Class="@FillerClassname" Spacing="1" Row>
@{
var value = _longestItem is { Value: not null } ? ToStringFunc ?.Invoke(_longestItem.Value) ?? ConvertSet(_longestItem.Value) : string.Empty;
var useLabel = Label is not null && Placeholder is null && !ShrinkLabel
var value = string.Empty;

if (_longestItem is { Value: not null })
{
value = ConvertSet(_longestItem.Value) ?? string.Empty;
}

var useLabel = Label is not null && Placeholder is null && !ShrinkLabel
&& (Adornment != Adornment.Start || (AdornmentIcon is null && AdornmentText is null));

var defaultValue = useLabel ? Label : Placeholder;
var showDefault = defaultValue?.Length > value.Length;
var hasChildContent = _longestItem?.ChildContent is not null;
var hasStringValue = ToStringFunc is not null && !string.IsNullOrEmpty(value);

var renderText = showDefault || !hasChildContent || MultiSelectionTextFunc is not null || hasStringValue;
}
@if (defaultValue?.Length > value!.Length)
{
<MudText Typo="@Typo">@(MultiSelectionTextFunc?.Invoke([defaultValue]) ?? defaultValue)</MudText>
}
else if (_longestItem?.ChildContent is null || MultiSelectionTextFunc is not null)

@if (renderText)
{
<MudText Typo="@Typo">@(MultiSelectionTextFunc?.Invoke([value!]) ?? value!)</MudText>
var text = showDefault ? defaultValue! : value;

<MudText Typo="@Typo">
@(MultiSelectionTextFunc?.Invoke([text]) ?? text)
</MudText>
}
else
{
<MudText Typo="@Typo">@_longestItem.ChildContent</MudText>
<MudText Typo="@Typo">@_longestItem!.ChildContent</MudText>
}

@AdornmentText

@if (AdornmentText is null)
{
<MudIcon Icon="@(AdornmentIcon ?? OpenIcon)" Size="IconSize"></MudIcon>
}

@if (Clearable)
{
<MudIcon Icon="@ClearIcon" Size="IconSize"></MudIcon>
Expand Down
Loading
Loading