Skip to content
Closed
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
@@ -1,4 +1,4 @@
ο»Ώ<FluentSelect TOption="ImageFit" Items="@(Enum.GetValues<ImageFit>())" @bind-Value="@_selectedFit" />
ο»Ώ<FluentSelect Items="@(Enum.GetValues<ImageFit>())" @bind-Value="@_selectedFit" />
<div style="width: auto; height: 400px; margin-top: 10px">
<FluentImage Fit="@_selectedFit" Source="/big-heart.jpg" AlternateText="Placeholder Image" />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ο»Ώ<FluentSelect TOption="ImageShape" Items="@(Enum.GetValues<ImageShape>())" @bind-Value="@_selectedShape" />
ο»Ώ<FluentSelect Items="@(Enum.GetValues<ImageShape>())" @bind-Value="@_selectedShape" />
<div style="width: auto; height: 300px; margin-top: 10px">
<FluentImage Shape="@_selectedShape" Source="/big-heart.jpg" AlternateText="Placeholder Image" />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ο»Ώ<FluentCombobox Label="Countries"
TOption="@SampleData.Olympics2024.Country"
TValue="@SampleData.Olympics2024.Country"
Placeholder="Select your countries"
Multiple="true"
OptionText="@(i => i?.Name)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
ο»Ώ<FluentSelect Label="RGB Color"
Placeholder="Select a color"
TOption="string"
TValue="string"
@bind-Value="@Value">
<FluentOption Value="#ff0000">Red</FluentOption>
<FluentOption Value="#00ff00">Green</FluentOption>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<FluentSelect Label="Color"
Placeholder="Select colors"
Items="@Colors"
TOption="string"
TValue="string"
@bind-SelectedItems="@SelectedItems"
Multiple="true" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<FluentSelect Items="@([true, false, null])"
OptionText="@(i => GetVisibleText(i) )"
TOption="bool?"
TValue="bool?"
@bind-Value="@Visible" />

@code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<FluentSelect Items="@([true, false, null])"
OptionText="@(i => GetVisibleText(i) )"
TOption="bool?"
TValue="bool?"
@bind-Value="@Visible" />

@code
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Components/DateTime/FluentTimePicker.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
{
<FluentCombobox @ref="_fluentCombobox"
Id="@Id"
TOption="DateTime ?"
TOption="DateTime?"
TValue="DateTime?"
Class="@ClassValue"
Style="@StyleValue"
Label="@Label"
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Components/DateTime/FluentTimePicker.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
public partial class FluentTimePicker<TValue> : FluentInputBase<TValue>
{
private DateTime DefaultTime => Culture.Calendar.MinSupportedDateTime;
private FluentCombobox<DateTime?> _fluentCombobox = default!;
private FluentCombobox<DateTime?, DateTime?> _fluentCombobox = default!;

/// <summary />
public FluentTimePicker(LibraryConfiguration configuration) : base(configuration)
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Components/List/FluentCombobox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// A FluentCombobox allows one option to be selected from multiple items.
/// </summary>
/// <typeparam name="TOption"></typeparam>
public partial class FluentCombobox<TOption> : FluentSelect<TOption>
/// <typeparam name="TValue"></typeparam>
public partial class FluentCombobox<TOption, TValue> : FluentSelect<TOption, TValue>
{
/// <summary />
public FluentCombobox(LibraryConfiguration configuration) : base(configuration) { }
Expand Down
5 changes: 4 additions & 1 deletion src/Core/Components/List/FluentListBase.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
ο»Ώ@namespace Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@inherits FluentInputBase<TOption>
@attribute [CascadingTypeParameter(nameof(TOption))]
@attribute [CascadingTypeParameter(nameof(TValue))]
@inherits FluentInputBase<TValue>
@typeparam TOption
@typeparam TValue

@code
{
Expand Down
59 changes: 34 additions & 25 deletions src/Core/Components/List/FluentListBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
namespace Microsoft.FluentUI.AspNetCore.Components;

/// <summary />
public abstract partial class FluentListBase<TOption> : FluentInputBase<TOption>, ITooltipComponent
public abstract partial class FluentListBase<TOption, TValue> : FluentInputBase<TValue>, ITooltipComponent, IInternalListBase<TValue>
{
// List of items rendered with an ID to retrieve the element by ID.
private Dictionary<string, TOption> InternalOptions { get; } = new(StringComparer.Ordinal);
private protected Dictionary<string, TOption> InternalOptions { get; } = new(StringComparer.Ordinal);

/// <summary />
[DynamicDependency(nameof(OnDropdownChangeHandlerAsync))]
Expand Down Expand Up @@ -74,7 +74,7 @@ protected FluentListBase(LibraryConfiguration configuration) : base(configuratio
public EventCallback<IEnumerable<TOption>> SelectedItemsChanged { get; set; }

/// <summary>
/// Gets or sets the template for the <see cref="FluentListBase{TOption}.Items"/> items.
/// Gets or sets the template for the <see cref="FluentListBase{TOption, TValue}.Items"/> items.
/// </summary>
[Parameter]
public virtual RenderFragment<TOption>? OptionTemplate { get; set; }
Expand All @@ -101,7 +101,7 @@ protected FluentListBase(LibraryConfiguration configuration) : base(configuratio
/// Gets or sets the function used to determine whether two options are considered equal for selection purposes.
/// </summary>
[Parameter]
public virtual Func<TOption?, TOption?, bool>? OptionSelectedComparer { get; set; }
public virtual Func<TValue?, TValue?, bool>? OptionSelectedComparer { get; set; }

/// <inheritdoc cref="ITooltipComponent.Tooltip" />
[Parameter]
Expand All @@ -114,7 +114,7 @@ protected override async Task OnInitializedAsync()
}

/// <summary />
internal string? AddOption(FluentOption option)
string? IInternalListBase<TValue>.AddOption(FluentOption option)
{
var id = option.Id ?? "";

Expand All @@ -135,7 +135,7 @@ protected override async Task OnInitializedAsync()
}

// Manual list using FluentOption
if (typeof(TOption) == typeof(string) && option.Value is TOption value)
if (option.Value is TOption value)
{
InternalOptions.TryAdd(id, value);
return option.Id;
Expand All @@ -145,14 +145,13 @@ protected override async Task OnInitializedAsync()
}

/// <summary />
internal string? RemoveOption(FluentOption option)
string? IInternalListBase<TValue>.RemoveOption(FluentOption option)
{
var id = option.Id ?? "";

if (InternalOptions.ContainsKey(id))
{
if (option.Data is TOption _ ||
typeof(TOption) == typeof(string) && option.Value is TOption _)
if (option.Data is TOption _ || option.Value is TOption _)
{
InternalOptions.Remove(id);
return option.Id;
Expand All @@ -170,21 +169,24 @@ protected virtual bool GetOptionSelected(TOption? item)
return false;
}

// Multiple items
if (Multiple)
if(TryParseValueFromString(GetOptionValue(item), out var itemValue, out _))
{
if (OptionSelectedComparer != null)
// Multiple items
if (Multiple)
{
return SelectedItems?.Any(selectedItem => OptionSelectedComparer(item, selectedItem)) ?? false;
}
if (OptionSelectedComparer != null)
{
return SelectedItems?.Any(selectedItem => TryParseValueFromString(GetOptionValue(selectedItem), out var selectedItemValue, out _) && OptionSelectedComparer(itemValue, selectedItemValue)) ?? false;
}

return SelectedItems?.Contains(item) ?? false;
}
return SelectedItems?.Contains(item) ?? false;
}

// Single item
if (OptionSelectedComparer != null)
{
return OptionSelectedComparer(item, CurrentValue);
// Single item
if (OptionSelectedComparer != null)
{
return OptionSelectedComparer(itemValue, CurrentValue);
}
}

return Equals(item, CurrentValue);
Expand Down Expand Up @@ -251,27 +253,34 @@ internal virtual async Task OnDropdownChangeHandlerAsync(DropdownEventArgs e)

if (ValueChanged.HasDelegate)
{
await ValueChanged.InvokeAsync(SelectedItems.FirstOrDefault());
if(SelectedItems.FirstOrDefault() is TValue value)
{
await ValueChanged.InvokeAsync(value);
}
else if(TryParseValueFromString(CurrentValueAsString, out var parsedValue, out _))
{
await ValueChanged.InvokeAsync(parsedValue);
}
}
}

/// <summary />
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TOption result, [NotNullWhen(false)] out string? validationErrorMessage)
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
return this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);
}

/// <summary>
/// For unit testing purposes only.
/// </summary>
internal bool InternalTryParseValueFromString(string? value, [MaybeNullWhen(false)] out TOption result, [NotNullWhen(false)] out string? validationErrorMessage)
internal bool InternalTryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
return TryParseValueFromString(value, out result, out validationErrorMessage);
}

/// <summary />
internal InternalListContext<TOption> GetCurrentContext()
internal InternalListContext<TValue> GetCurrentContext()
{
return new InternalListContext<TOption>(this);
return new InternalListContext<TValue>(this);
}
}
7 changes: 5 additions & 2 deletions src/Core/Components/List/FluentSelect.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
ο»Ώ@namespace Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.FluentUI.AspNetCore.Components.Extensions
@inherits FluentListBase<TOption>
@inherits FluentListBase<TOption, TValue>
@attribute [CascadingTypeParameter(nameof(TOption))]
@attribute [CascadingTypeParameter(nameof(TValue))]
@typeparam TOption
@typeparam TValue

<CascadingValue Value="GetCurrentContext()" Name="ListContext" TValue="InternalListContext<TOption>" IsFixed="true">
<CascadingValue Value="GetCurrentContext()" Name="ListContext" TValue="InternalListContext<TValue>" IsFixed="true">
<FluentField InputComponent="@this" ForId="@Id" Class="@ClassValue" Style="@StyleValue">
<fluent-dropdown id="@Id"
appearance="@(Appearance.ToAttributeValue(isNull: ListAppearance.Outline, returnEmptyAsNull: true))"
Expand Down
44 changes: 41 additions & 3 deletions src/Core/Components/List/FluentSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// A FluentSelect allows for selecting one or more options from a list of options.
/// </summary>
/// <typeparam name="TOption"></typeparam>
public partial class FluentSelect<TOption> : FluentListBase<TOption>
/// <typeparam name="TValue"></typeparam>
public partial class FluentSelect<TOption, TValue> : FluentListBase<TOption, TValue>
{
private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "List/FluentSelect.razor.js";

Expand Down Expand Up @@ -56,9 +57,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)

// By default, the combobox text is not bound to the Value property.
// This method don't change the SelectedItems and Value properties.
if (string.Equals(DropdownType, "combobox", StringComparison.Ordinal))
if (string.Equals(DropdownType, "combobox", StringComparison.Ordinal) && Value is TOption optionValue)
{
var defaultText = GetOptionText(Value);
var defaultText = GetOptionText(optionValue);
await JSModule.ObjectReference.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Select.SetComboBoxValue", Id, defaultText);
}
}
Expand All @@ -81,4 +82,41 @@ public async Task ClearAsync()
await SelectedItemsChanged.InvokeAsync(SelectedItems);
}
}

internal override async Task OnDropdownChangeHandlerAsync(DropdownEventArgs e)
{
// List of IDs received from the web component.
var selectedIds = e.SelectedOptions?.Split(';', StringSplitOptions.TrimEntries) ?? Array.Empty<string>();
SelectedItems = selectedIds.Length > 0
? InternalOptions.Where(kvp => selectedIds.Contains(kvp.Key, StringComparer.Ordinal)).Select(kvp => kvp.Value).ToList()
: Array.Empty<TOption>();

if (SelectedItemsChanged.HasDelegate)
{
await SelectedItemsChanged.InvokeAsync(SelectedItems);
}

if (ValueChanged.HasDelegate)
{
if (InternalOptions.Count == 0 && selectedIds.Length > 0)
{
var result = await JSModule.ObjectReference.InvokeAsync<string>("Microsoft.FluentUI.Blazor.Select.GetSelectedValue", selectedIds.FirstOrDefault());
if (TryParseValueFromString(result, out var currentValue, out _))
{
await ValueChanged.InvokeAsync(currentValue);
}
}
else
{
if (SelectedItems.FirstOrDefault() is TValue value)
{
await ValueChanged.InvokeAsync(value);
}
else if (TryParseValueFromString(CurrentValueAsString, out var parsedValue, out _))
{
await ValueChanged.InvokeAsync(parsedValue);
}
}
}
}
}
17 changes: 17 additions & 0 deletions src/Core/Components/List/FluentSelect.razor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,21 @@ export namespace Microsoft.FluentUI.Blazor.Select {
element._control.value = value;
}
}

/**
* Fluent-specific helper:
* Retrieves the selected option value from a fluent-option.
* @param id
*/
export function GetSelectedValue(id: string): string | null {
const element = document.getElementById(id) as any;

if (!element) return null;

if (element.tagName === 'FLUENT-OPTION' && element.value !== undefined) {
return element.value?.toString() ?? null;
}

return null;
}
}
18 changes: 18 additions & 0 deletions src/Core/Components/List/IInternalListBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

// ------------------------------------------------------------------------
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------

namespace Microsoft.FluentUI.AspNetCore.Components;

internal interface IInternalListBase<TValue>
{
string? AddOption(FluentOption option);

string? RemoveOption(FluentOption option);

Func<TValue?, TValue?, bool>? OptionSelectedComparer { get; set; }
}
4 changes: 2 additions & 2 deletions src/Core/Components/List/IInternalListContextOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
internal interface IInternalListContextOptions
{
/// <summary>
/// Adds an option to the <see cref="FluentListBase{TOption}"/>
/// Adds an option to the <see cref="FluentListBase{TOption,TValue}"/>
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
string? AddOption(FluentOption option);

/// <summary>
/// Removes an option to the <see cref="FluentListBase{TOption}"/>
/// Removes an option to the <see cref="FluentListBase{TOption, TValue}"/>
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
Expand Down
Loading
Loading