Skip to content

fix(select): select value(s) provided in @bind-Value(s) after async initialization#246

Merged
desmondinho merged 6 commits intomainfrom
fix/select-component-with-default-bind-value
Sep 5, 2025
Merged

fix(select): select value(s) provided in @bind-Value(s) after async initialization#246
desmondinho merged 6 commits intomainfrom
fix/select-component-with-default-bind-value

Conversation

@desmondinho
Copy link
Contributor

@desmondinho desmondinho commented Sep 5, 2025

Closes #245

This PR fixes an issue where values provided via @bind-Value(s) were not selected after async initialization:

<LumexSelect @bind-Value="@_selectedValue">
    @foreach( var item in _values )
    {
        <LumexSelectItem @key="@item" Value="@item">
            @item
        </LumexSelectItem>
    }
</LumexSelect>

@code {
    private string[] _values = ["20", "25", "40", "50"];
    private string? _selectedValue;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay( 1000 );

        _selectedValue = "20"; @* <=== should be displayed as selected value *@
    }
}

The fix ensures that selected item(s) are always updated when the component’s parameters are set, if:

  1. Value/Values are provided
  2. Items collection is not empty

Checklist

  • My code follows the project's coding style and guidelines.
  • I have included inline docs for my changes, where applicable.
  • I have added, updated or removed tests according to my changes.
  • All tests are passing.
  • There's an open issue for the PR that I am making.

Summary by CodeRabbit

  • New Features
    • Select reliably applies preselected value(s) on first render and after asynchronous initialization (single & multiple modes).
  • Bug Fixes
    • Fixed initial-selection synchronization so bound value(s) appear consistently.
    • Consistent handling of multiple-selection state when rendering the menu.
  • Refactor
    • Internal renaming and cleanup to simplify selection synchronization (no public API changes).
  • Tests
    • Added async initialization tests to validate selection after delayed setup.

@desmondinho desmondinho linked an issue Sep 5, 2025 that may be closed by this pull request
@desmondinho desmondinho self-assigned this Sep 5, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

Removes the old internal _ItemsCollectedNotifier and adds a relocated ItemsCollectedNotifier<TValue> implementation. Renames SelectContext.IsMultiSelect to IsMultipleSelect. Updates LumexSelect to use the new notifier and perform selection sync in OnParametersSet. Adds test wrapper and async initialization tests.

Changes

Cohort / File(s) Summary
Notifier refactor and relocation
src/LumexUI/Components/Select/Core/_ItemsCollectedNotifier.cs, src/LumexUI/Components/Select/Internal/ItemsCollectedNotifier.cs
Removes legacy _ItemsCollectedNotifier<TValue> and adds ItemsCollectedNotifier<TValue> (non-rendering IComponent) that performs first-render selection synchronization using the renamed IsMultipleSelect flag.
SelectContext flag rename
src/LumexUI/Components/Select/SelectContext.cs
Renames public property IsMultiSelectIsMultipleSelect.
Select component integration
src/LumexUI/Components/Select/LumexSelect.razor, src/LumexUI/Components/Select/LumexSelect.razor.cs
Switches markup to ItemsCollectedNotifier; replaces _context.IsMultiSelect with _context.IsMultipleSelect; adds selection synchronization in OnParametersSet (calls UpdateSelectedItem(s) before slot rebuild).
Tests: async init support and imports
tests/LumexUI.Tests/Components/Select/SelectTests.razor, tests/LumexUI.Tests/Wrappers/OnInitializedAsyncWrapper.razor, tests/LumexUI.Tests/_Imports.razor
Adds OnInitializedAsyncWrapper test helper and imports; introduces tests that assert selected value(s) after asynchronous initialization; applies formatting/spacing normalizations in tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as Page/Parent
  participant Select as LumexSelect<TValue>
  participant Notifier as ItemsCollectedNotifier<TValue>
  participant Ctx as SelectContext<TValue>

  User->>Page: Navigate / Render
  Page->>Select: Render with @bind-Value / @bind-Values
  Note over Select: First render
  Select->>Notifier: Instantiate non-visual notifier
  Notifier->>Ctx: If IsMultipleSelect -> UpdateSelectedItems(Select.Values)
  Notifier->>Ctx: Else -> UpdateSelectedItem(Select.Value)
  Note over Ctx: Selection state initialized
  Note over Select: Later parameter updates (sync in OnParametersSet)
  Page->>Select: Parameters updated (e.g., set in OnInitializedAsync)
  Select->>Ctx: UpdateSelectedItem(s) before slot rebuild
  Select->>Page: Re-render with correct selected value(s)
Loading
sequenceDiagram
  autonumber
  participant Test as Test Runner
  participant Wrap as OnInitializedAsyncWrapper
  participant Select as LumexSelect<TValue>
  participant Ctx as SelectContext<TValue>

  Test->>Wrap: Render wrapper with ChildContent (Select) and Action
  Wrap->>Wrap: OnInitializedAsync (delay)
  Wrap-->>Wrap: Invoke Action() to set Value/Values
  Wrap->>Select: Parameter update
  Select->>Ctx: OnParametersSet -> UpdateSelectedItem(s)
  Test->>Test: WaitForAssertion
  Test-->>Select: Assert selected value(s) present
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Select binds default value correctly when set during OnInitialized/OnInitializedAsync (#245)

Poem

I twitch my whiskers at the gleam of state,
A dropdown hums—its choices now relate.
First render whispers, “Pick the right one, please,”
Then async winds confirm it with a breeze.
I thump with joy—selects align just so. 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/select-component-with-default-bind-value

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@desmondinho desmondinho removed their assignment Sep 5, 2025
@codecov
Copy link

codecov bot commented Sep 5, 2025

Codecov Report

❌ Patch coverage is 94.63087% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.13%. Comparing base (19b89dc) to head (dba491b).
⚠️ Report is 127 commits behind head on main.

Files with missing lines Patch % Lines
src/LumexUI/Components/Select/LumexSelect.razor.cs 95.56% 1 Missing and 5 partials ⚠️
...mponents/Select/Internal/ItemsCollectedNotifier.cs 83.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #246      +/-   ##
==========================================
- Coverage   96.95%   93.13%   -3.82%     
==========================================
  Files          70      144      +74     
  Lines        1542     2562    +1020     
  Branches      150      390     +240     
==========================================
+ Hits         1495     2386     +891     
- Misses         28       92      +64     
- Partials       19       84      +65     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/LumexUI/Components/Select/SelectContext.cs (1)

42-55: Avoid stale selection when single-value doesn’t match any item

If currentValue is null or not found in Items, SelectedItems remains as-is. That can leave a stale selection after items or value change. Clear it to reflect “no selection.”

Apply:

 public void UpdateSelectedItem( TValue? currentValue )
 {
-    if( currentValue is null )
-    {
-        return;
-    }
-
-    var item = Items.Find( i => EqualityComparer<TValue>.Default.Equals( currentValue, i.Value ) );
-    if( item is not null )
-    {
-        SelectedItems.Clear();
-        SelectedItems.Add( item );
-    }
+    if( currentValue is null )
+    {
+        SelectedItems.Clear();
+        return;
+    }
+
+    var item = Items.Find( i => EqualityComparer<TValue>.Default.Equals( currentValue, i.Value ) );
+    SelectedItems.Clear();
+    if( item is not null )
+    {
+        SelectedItems.Add( item );
+    }
 }
🧹 Nitpick comments (6)
src/LumexUI/Components/Select/SelectContext.cs (1)

57-69: Minor: drop null-forgiving and use sets for marginally cleaner logic

If i.Value can be nullable for TValue, Contains(i.Value) is safe with EqualityComparer<TValue>.Default. Consider a set for membership checks; current approach is fine performance-wise given small N.

- var matchedItems = Items.Where( i => valueSet.Contains( i.Value! ) ).ToList();
+ var matchedItems = Items.Where( i => valueSet.Contains( i.Value ) ).ToList();
src/LumexUI/Components/Select/LumexSelect.razor.cs (1)

171-217: Document fixed selection mode constraint
Selection mode (_context.IsMultipleSelect) is inferred only once on the first SetParametersAsync call (guarded by _hasInitializedParameters). If you ever need to support runtime switching between single- and multiple-select, explicitly document that changes to Value/Values after initialization won’t update the mode.

tests/LumexUI.Tests/Wrappers/OnInitializedAsyncWrapper.razor (1)

9-14: Make the wrapper deterministic and async-friendly

A fixed 500ms delay slows tests and can be flaky. Prefer a parameterized delay and Func<Task> callback so tests can await deterministic signals.

 @code {
-    [Parameter] public RenderFragment? ChildContent { get; set; }
-    [Parameter] public Action? Action { get; set; }
+    [Parameter] public RenderFragment? ChildContent { get; set; }
+    [Parameter] public Func<Task>? Action { get; set; }
+    [Parameter] public int DelayMs { get; set; } = 1;
 
     protected override async Task OnInitializedAsync()
     {
-        await Task.Delay( 500 );
-
-        Action?.Invoke();
+        await Task.Delay( DelayMs );
+        if( Action is not null )
+        {
+            await Action();
+        }
     }
 }
src/LumexUI/Components/Select/Internal/ItemsCollectedNotifier.cs (3)

30-52: Simplify SetParametersAsync with early return.

Minor readability: invert the condition and drop the else.

-Task IComponent.SetParametersAsync( ParameterView parameters )
-{
-    if( _isFirstRender )
-    {
-        parameters.SetParameterProperties( this );
-        ...
-        _isFirstRender = false;
-        return Task.CompletedTask;
-    }
-    else
-    {
-        return Task.CompletedTask;
-    }
-}
+Task IComponent.SetParametersAsync( ParameterView parameters )
+{
+    if( !_isFirstRender )
+        return Task.CompletedTask;
+
+    parameters.SetParameterProperties( this );
+    ...
+    _isFirstRender = false;
+    return Task.CompletedTask;
+}

36-43: Optional: Skip work when no items are collected yet.

For multi-select, updating with an empty Items set does nothing but adds cycles. Guarding keeps intent explicit.

-        if( Context.IsMultipleSelect )
+        if( Context.IsMultipleSelect )
         {
-            Context.UpdateSelectedItems( Select.Values );
+            if( Context.Items.Count > 0 )
+                Context.UpdateSelectedItems( Select.Values );
         }
         else
         {
-            Context.UpdateSelectedItem( Select.Value );
+            if( Context.Items.Count > 0 )
+                Context.UpdateSelectedItem( Select.Value );
         }

5-10: Remove the redundant IComponent alias.

using Microsoft.AspNetCore.Components; already brings IComponent into scope.

 using System.ComponentModel;
 
 using Microsoft.AspNetCore.Components;
 
-using IComponent = Microsoft.AspNetCore.Components.IComponent;
-
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bee81f4 and 16d9dd2.

📒 Files selected for processing (8)
  • src/LumexUI/Components/Select/Core/_ItemsCollectedNotifier.cs (0 hunks)
  • src/LumexUI/Components/Select/Internal/ItemsCollectedNotifier.cs (1 hunks)
  • src/LumexUI/Components/Select/LumexSelect.razor (2 hunks)
  • src/LumexUI/Components/Select/LumexSelect.razor.cs (1 hunks)
  • src/LumexUI/Components/Select/SelectContext.cs (1 hunks)
  • tests/LumexUI.Tests/Components/Select/SelectTests.razor (9 hunks)
  • tests/LumexUI.Tests/Wrappers/OnInitializedAsyncWrapper.razor (1 hunks)
  • tests/LumexUI.Tests/_Imports.razor (1 hunks)
💤 Files with no reviewable changes (1)
  • src/LumexUI/Components/Select/Core/_ItemsCollectedNotifier.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-30T00:22:33.596Z
Learnt from: desmondinho
PR: LumexUI/lumexui#159
File: src/LumexUI.Motion/LumexUI.Motion.csproj:9-11
Timestamp: 2025-01-30T00:22:33.596Z
Learning: LumexUI.Motion is planned to be distributed as a separate NuGet package with its own test coverage, hence the assembly-wide coverage exclusion in the project.

Applied to files:

  • tests/LumexUI.Tests/_Imports.razor
🧬 Code graph analysis (2)
src/LumexUI/Components/Select/Internal/ItemsCollectedNotifier.cs (2)
src/LumexUI/Components/Select/SelectContext.cs (3)
  • SelectContext (5-70)
  • UpdateSelectedItems (57-69)
  • UpdateSelectedItem (42-55)
src/LumexUI/Components/Select/Core/_ItemsCollectedNotifier.cs (1)
  • _ItemsCollectedNotifier (17-57)
src/LumexUI/Components/Select/LumexSelect.razor.cs (1)
src/LumexUI/Components/Select/SelectContext.cs (3)
  • SelectContext (5-70)
  • UpdateSelectedItem (42-55)
  • UpdateSelectedItems (57-69)
🔇 Additional comments (11)
tests/LumexUI.Tests/_Imports.razor (1)

11-12: LGTM: imports for test helpers/wrappers

Brings the new namespaces into scope for the tests as intended.

src/LumexUI/Components/Select/SelectContext.cs (1)

12-12: Rename reads better and aligns with common nomenclature

IsMultipleSelect is clearer than IsMultiSelect. No functional concerns.

src/LumexUI/Components/Select/LumexSelect.razor (2)

98-153: LGTM: multi/single paths switched to IsMultipleSelect

Conditional rendering now matches the renamed flag. Looks consistent with context behavior.


16-16: No action required: Internal namespace already imported

The @using LumexUI.Internal directive is present in src/LumexUI/_Imports.razor, so ItemsCollectedNotifier will be discovered without additional qualifiers.

src/LumexUI/Components/Select/LumexSelect.razor.cs (4)

141-144: LGTM: HasValue logic matches single/multiple semantics

Uses IsMultipleSelect and checks count vs. null appropriately.


196-207: Good guard: disallow binding both Value and Values; set selection mode once

The validation and the initial IsMultipleSelect inference are solid.


279-286: LGTM: single-value change flow updates context and closes popover

Matches expected UX and keeps context in sync.


288-292: LGTM: multi-value change flow updates context

Consistent with single-value path.

tests/LumexUI.Tests/Components/Select/SelectTests.razor (3)

143-161: Great coverage for async initialization paths.

These two tests capture the regression described in #245 and validate the fix for both single and multiple selection.

Also applies to: 163-181


168-169: No action needed: C# 12 collection expressions are supported by net9.0
The test project targets net9.0, which defaults to LatestMajor C# (C# 12+), so the ["penguin"] expression compiles without adding a LangVersion.


148-155: Ensure OnInitializedAsyncWrapper triggers a re-render after invoking the Action. Confirm that the wrapper implementation awaits InvokeAsync(StateHasChanged) immediately after executing the provided Action.

@desmondinho desmondinho merged commit c5a8cda into main Sep 5, 2025
3 of 5 checks passed
@desmondinho desmondinho deleted the fix/select-component-with-default-bind-value branch September 5, 2025 20:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/LumexUI/Components/Select/LumexSelect.razor.cs (1)

224-231: Good fix on single vs. multiple branch selection

Branching on _context.IsMultipleSelect resolves the earlier multi-select path being skipped when TValue is a struct.

🧹 Nitpick comments (2)
src/LumexUI/Components/Select/LumexSelect.razor.cs (2)

141-144: Make HasValue reflect actual selection state (avoids default(TValue) ambiguity)

Optional but safer: compute from _context.SelectedItems so UI “filled” state mirrors actual selection.

-private bool HasValue => _context.IsMultipleSelect
-  ? CurrentValues is { Count: > 0 }
-  : CurrentValue is not null;
+private bool HasValue => _context.SelectedItems.Count > 0;

274-279: Also gate trigger on Disabled

Prevents opening the popover when the component is disabled.

-  if( _popoverRef is null || ReadOnly )
+  if( _popoverRef is null || ReadOnly || Disabled )
   {
     return Task.CompletedTask;
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 16d9dd2 and dba491b.

📒 Files selected for processing (1)
  • src/LumexUI/Components/Select/LumexSelect.razor.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/LumexUI/Components/Select/LumexSelect.razor.cs (1)
src/LumexUI/Components/Select/SelectContext.cs (3)
  • SelectContext (5-70)
  • UpdateSelectedItem (42-55)
  • UpdateSelectedItems (57-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-test
🔇 Additional comments (3)
src/LumexUI/Components/Select/LumexSelect.razor.cs (3)

208-214: LGTM: sensible default for LabelPlacement

Defaulting to Outside when neither Label nor LabelPlacement is provided improves UX and matches expectations.


282-295: LGTM: selection state stays in sync after user interaction

Updating the context after committing value(s) and closing the popover is correct and keeps UI consistent.


191-217: Confirm runtime switching between single and multiple is unsupported

_context.IsMultipleSelect is latched on first parameter set. If consumers switch from @bind-Value to @bind-Values (or vice versa) later, behavior is undefined. If unsupported, consider documenting; if supported, recompute each call.

Would you like me to add a doc comment and guard that throws if the binding mode changes after initialization?

Comment on lines +170 to +172
private bool _isOpen;
private bool _hasInitializedParameters;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Track parameter presence flags for use during OnParametersSet

Needed by the previous comment to align behavior with the PR description.

   private bool _isOpen;
   private bool _hasInitializedParameters;
+  private bool _valueParamProvided;
+  private bool _valuesParamProvided;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private bool _isOpen;
private bool _hasInitializedParameters;
private bool _isOpen;
private bool _hasInitializedParameters;
private bool _valueParamProvided;
private bool _valuesParamProvided;

Comment on lines +191 to +217
/// <inheritdoc />
public override async Task SetParametersAsync( ParameterView parameters )
{
await base.SetParametersAsync( parameters );

if( !_hasInitializedParameters )
{
if( parameters.TryGetValue<TValue>( nameof( Value ), out var _ ) &&
parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out var _ ) )
{
throw new InvalidOperationException(
$"{GetType()} requires one of {nameof( Value )} or {nameof( Values )}, but both were specified." );
}

// Set the selection mode depending on the 2-way bindable parameter.
_context.IsMultipleSelect = parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out _ );

// Set the LabelPlacement to 'Outside' by default if both Label and LabelPlacement are not specified.
if( !parameters.TryGetValue<string>( nameof( Label ), out var _ ) &&
!parameters.TryGetValue<LabelPlacement>( nameof( LabelPlacement ), out var _ ) )
{
LabelPlacement = LabelPlacement.Outside;
}

_hasInitializedParameters = true;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix Value/Values presence detection (value-type bug) and enforce the mutual-exclusion check on every parameter set

  • Using TryGetValue<TValue> will not detect [Parameter] TValue? Value when TValue is a non-nullable struct (e.g., int), so the “both specified” guard can silently fail.
  • The mutual-exclusion check currently runs only once; it should run on every SetParametersAsync call to catch later updates.

Apply:

 public override async Task SetParametersAsync( ParameterView parameters )
 {
   await base.SetParametersAsync( parameters );
 
-  if( !_hasInitializedParameters )
-  {
-    if( parameters.TryGetValue<TValue>( nameof( Value ), out var _ ) &&
-        parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out var _ ) )
-    {
-      throw new InvalidOperationException(
-        $"{GetType()} requires one of {nameof( Value )} or {nameof( Values )}, but both were specified." );
-    }
- 
-    // Set the selection mode depending on the 2-way bindable parameter.
-    _context.IsMultipleSelect = parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out _ );
+  // Detect presence of parameters for this render (works for both reference and value types)
+  var hasValueParam = parameters.TryGetValue<object>( nameof( Value ), out _ );
+  var hasValuesParam = parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out _ );
+
+  // Persist for use in OnParametersSet
+  _valueParamProvided = hasValueParam;
+  _valuesParamProvided = hasValuesParam;
+
+  // Enforce mutual exclusivity on every update
+  if( hasValueParam && hasValuesParam )
+  {
+    throw new InvalidOperationException(
+      $"{GetType()} requires one of {nameof( Value )} or {nameof( Values )}, but both were specified." );
+  }
+
+  if( !_hasInitializedParameters )
+  {
+    // Set the selection mode depending on the 2-way bindable parameter.
+    _context.IsMultipleSelect = hasValuesParam;
 
     // Set the LabelPlacement to 'Outside' by default if both Label and LabelPlacement are not specified.
     if( !parameters.TryGetValue<string>( nameof( Label ), out var _ ) &&
         !parameters.TryGetValue<LabelPlacement>( nameof( LabelPlacement ), out var _ ) )
     {
       LabelPlacement = LabelPlacement.Outside;
     }
 
     _hasInitializedParameters = true;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <inheritdoc />
public override async Task SetParametersAsync( ParameterView parameters )
{
await base.SetParametersAsync( parameters );
if( !_hasInitializedParameters )
{
if( parameters.TryGetValue<TValue>( nameof( Value ), out var _ ) &&
parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out var _ ) )
{
throw new InvalidOperationException(
$"{GetType()} requires one of {nameof( Value )} or {nameof( Values )}, but both were specified." );
}
// Set the selection mode depending on the 2-way bindable parameter.
_context.IsMultipleSelect = parameters.TryGetValue<ICollection<TValue>>( nameof( Values ), out _ );
// Set the LabelPlacement to 'Outside' by default if both Label and LabelPlacement are not specified.
if( !parameters.TryGetValue<string>( nameof( Label ), out var _ ) &&
!parameters.TryGetValue<LabelPlacement>( nameof( LabelPlacement ), out var _ ) )
{
LabelPlacement = LabelPlacement.Outside;
}
_hasInitializedParameters = true;
}
}
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
// Detect presence of parameters for this render (works for both reference and value types)
var hasValueParam = parameters.TryGetValue<object>(nameof(Value), out _);
var hasValuesParam = parameters.TryGetValue<ICollection<TValue>>(nameof(Values), out _);
// Persist for use in OnParametersSet
_valueParamProvided = hasValueParam;
_valuesParamProvided = hasValuesParam;
// Enforce mutual exclusivity on every update
if (hasValueParam && hasValuesParam)
{
throw new InvalidOperationException(
$"{GetType()} requires one of {nameof(Value)} or {nameof(Values)}, but both were specified.");
}
if (!_hasInitializedParameters)
{
// Set the selection mode depending on the 2-way bindable parameter.
_context.IsMultipleSelect = hasValuesParam;
// Set the LabelPlacement to 'Outside' by default if both Label and LabelPlacement are not specified.
if (!parameters.TryGetValue<string>(nameof(Label), out _) &&
!parameters.TryGetValue<LabelPlacement>(nameof(LabelPlacement), out _))
{
LabelPlacement = LabelPlacement.Outside;
}
_hasInitializedParameters = true;
}
}
🤖 Prompt for AI Agents
In src/LumexUI/Components/Select/LumexSelect.razor.cs around lines 191–217, the
presence checks use the generic TryGetValue<TValue> which fails for non-nullable
value types and the mutual-exclusion guard only runs once; change the presence
detection to use parameters.TryGetValue<object>(nameof(Value), out _) and
parameters.TryGetValue<object>(nameof(Values), out _) so presence is detected
regardless of TValue being a struct, move the mutual-exclusion check to run on
every SetParametersAsync invocation (before returning and not gated by
_hasInitializedParameters), and also update _context.IsMultipleSelect on every
call (leave the LabelPlacement defaulting behavior as required, but ensure its
presence check uses the same object-based TryGetValue).

Comment on lines +222 to +232
if( _context.Items.Count > 0 )
{
if( !_context.IsMultipleSelect )
{
_context.UpdateSelectedItem( Value );
}
else
{
_context.UpdateSelectedItems( Values );
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Gate selection sync on whether the corresponding parameter was provided to avoid accidental default(TValue) picks

Without checking parameter presence, single-select can auto-select an item whose value equals default(TValue) (e.g., 0) even when the user didn’t bind Value. This violates the PR’s “only when Value/Values are provided” intent.

Apply:

-    if( _context.Items.Count > 0 )
-    {
-      if( !_context.IsMultipleSelect )
-      {
-        _context.UpdateSelectedItem( Value );
-      }
-      else
-      {
-        _context.UpdateSelectedItems( Values );
-      }
-    }
+    if( _context.Items.Count > 0 )
+    {
+      if( !_context.IsMultipleSelect )
+      {
+        if( _valueParamProvided )
+        {
+          _context.UpdateSelectedItem( Value );
+        }
+      }
+      else
+      {
+        if( _valuesParamProvided )
+        {
+          _context.UpdateSelectedItems( Values );
+        }
+      }
+    }

Note: This relies on the two new flags set in SetParametersAsync.

🤖 Prompt for AI Agents
In src/LumexUI/Components/Select/LumexSelect.razor.cs around lines 222-232, the
selection synchronization currently updates the selected item(s) unconditionally
when items exist, which can cause a single-select to pick a default(TValue) when
the Value parameter was not provided; change the gates so you only call
UpdateSelectedItem/UpdateSelectedItems when the corresponding parameter was
actually provided by checking the two flags set in SetParametersAsync (e.g.,
only call UpdateSelectedItem(Value) if the Value-was-set flag is true, and only
call UpdateSelectedItems(Values) if the Values-was-set flag is true), keeping
the Items.Count > 0 check as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Select is not displaying default value after async initialization

1 participant