Vue.js Gotcha: Single-Option Dropdowns Don’t Trigger v-model Updates

📖 2 minutes read

You’ve built a beautiful form. User selects “Project Manager” from a dropdown. Form submits. Backend throws: role_id is required.

What happened? The dropdown had only one option. Vue’s v-model never fired.

The Problem

Vue’s v-model on <select> elements only updates when a @change event fires.

Single-option dropdowns never fire @change because the user can’t change anything.

What the User Sees

<select v-model="form.role_id">
  <option value="3">Project Manager</option>
</select>

UI shows: “Project Manager” (looks selected).

Reality: form.role_id = null.

Why It Breaks

  1. Component initializes with form.role_id = null
  2. Dropdown renders with one option
  3. Browser displays that option as “selected” (visual default)
  4. No user interaction = no @change event
  5. v-model never updates
  6. Form submits null

The Solutions

Option 1: Auto-Select in Lifecycle Hook (Recommended)

mounted() {
  // Auto-select if only one option
  if (this.roleOptions.length === 1) {
    this.form.role_id = this.roleOptions[0].id;
  }
}

Pros: Clean, explicit, works with any UI library.

Cons: Needs lifecycle hook in every component with single-option dropdowns.

Option 2: Backend Defensive Handling

public function assignRole(Request $request)
{
    // Fallback to default if frontend didn't send value
    $roleId = $request->input('role_id') 
        ?? Role::getDefaultForContext($request->input('department_id'));
    
    // Validate the final value
    $request->merge(['role_id' => $roleId]);
    $validated = $request->validate([
        'role_id' => 'required|exists:roles,id',
    ]);
    
    // ... proceed with assignment
}

Pros: Fails gracefully, works even if frontend changes.

Cons: Backend needs context to know which default to use.

Option 3: Disable Dropdown When Single Option

<select 
  v-model="form.role_id" 
  :disabled="roleOptions.length === 1"
>
  <option 
    v-for="role in roleOptions" 
    :key="role.id" 
    :value="role.id"
  >
    {{ role.name }}
  </option>
</select>

Pros: Visual clarity that choice is locked.

Cons: Still need lifecycle hook or backend fallback.

Recommended Approach: Defense in Depth

Combine Option 1 + Option 2:

  1. Frontend: Auto-select in mounted()
  2. Backend: Defensive null handling with context-aware defaults

This way, if the frontend breaks (JS error, race condition, browser quirk), the backend still handles it gracefully.

The Lesson

Never trust frontend validation alone. Single-option dropdowns are a UI/UX trap—they look selected but aren’t.

Always validate and default on the backend. Your future self (and your error logs) will thank you.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *