Skip to content

Latest commit

 

History

History
178 lines (148 loc) · 4.59 KB

File metadata and controls

178 lines (148 loc) · 4.59 KB

Select Dropdown Architecture

Simple Flow Diagram

User clicks Release dropdown
        ↓
[Dropdown opens, shows: release-v1.0, release-v1.1, release-v2.0]
        ↓
User clicks "release-v1.0"
        ↓
bits-ui Select.Root detects selection
        ↓
bits-ui updates internal `value` prop to "release-v1.0"
        ↓
Svelte's `bind:value` syncs to component state
        ↓
createForm.releaseId = "release-v1.0"  ✅ STRING VALUE
        ↓
Form submitted with correct data to API

Code Flow

SelectWrapper.svelte (wrapper)
    ↓
    └─→ Exposes: bind:value={formData.releaseId}
    
bits-ui SelectPrimitive.Root (library)
    ↓
    └─→ type="single" → Single-select mode
    └─→ bind:value → Bindable value prop
    └─→ Handles dropdown UI and selection logic

Result: formData.releaseId = "string-value" ✅

Component Hierarchy

Stages.svelte
    ↓
    <Select bind:value={createForm.releaseId}>
        ↓
        SelectWrapper.svelte (custom, 7 lines)
            ↓
            <SelectPrimitive.Root type="single" bind:value>
                ↓
                bits-ui's native Select implementation
                    - Manages dropdown state
                    - Handles mouse/keyboard events
                    - Maintains value as string
                    ↓
                    <SelectTrigger>, <SelectContent>, <SelectItem>
                    ↓
                    Result: formData.releaseId = "my-release"

Data Flow

Before Fix (Broken):
Release selection → bits-ui → ??? → createForm.releaseId = ["release"]  ❌

After Fix (Working):
Release selection → bits-ui.Root 
                        ↓
                   type="single" (enforces string)
                        ↓
                   bind:value (Svelte binding)
                        ↓
                   createForm.releaseId = "release"  ✅

The Key Insight

bits-ui Select.Root Properties:
┌─────────────────────────────────────────────────┐
│ type = "single"                                 │
│   └─→ AUTOMATICALLY handles: STRING values      │
│                                                 │
│ type = "multiple"                               │
│   └─→ AUTOMATICALLY handles: ARRAY values       │
│                                                 │
│ bind:value                                      │
│   └─→ SUPPORTS both via Svelte's binding       │
└─────────────────────────────────────────────────┘

Our wrapper just exposes this:
type="single" + bind:value = Single-select string dropdown ✅

Why Previous Approach Failed

Old Approach:
- Try to intercept with onValueChange callback
- Attempt to extract value from bits-ui response
- Add custom conversion logic
- Result: Confused state management, unexpected types

New Approach:
- Use bits-ui's native type="single" behavior
- Use Svelte's native bind:value binding
- Let bits-ui handle the complexity
- Result: Clean state management, correct types

The Wrapper's Job

SelectWrapper.svelte does ONE thing:

Take bits-ui's Select.Root
  ↓
Set type="single" (enforces single-select)
  ↓
Expose via bind:value (Svelte binding)
  ↓
Pass through all other props
  ↓
That's it! 7 lines, no complexity.

Comparison

Before (18 lines, complex)

<script>
  let { value = $bindable(''), children, ...restProps } = $props()
  
  function handleSelectionChange(selected: any) {
    if (selected !== undefined && selected !== null) {
      value = selected  // ← Trying to extract from complex object
    }
  }
</script>

<SelectPrimitive.Root
  type="single"
  onValueChange={handleSelectionChange}  // ← Custom callback
  {...restProps}
>
  {@render children?.()}
</SelectPrimitive.Root>

After (7 lines, simple)

<script lang="ts">
  import { Select as SelectPrimitive } from 'bits-ui'
  
  let { value = $bindable(''), children, ...restProps } = $props()
</script>

<SelectPrimitive.Root type="single" bind:value {...restProps}>
  {@render children?.()}
</SelectPrimitive.Root>

Result

Input: User selects "release-v1.0" from dropdown
    ↓
Processing: bits-ui's type="single" + bind:value
    ↓
Output: createForm.releaseId = "release-v1.0"
    ↓
Format: String ✅
Type: Correct for API ✅
Count: Single value ✅
Behavior: Single-select only ✅

This is the complete, correct solution.