Your Dropdown Takes 38 Seconds: Open Modal on Field Click in Odoo 18
By Braincuber Team
Published on December 20, 2025
Your custom Odoo form has a "Location" field. Users need to select State + City from 50 states and 19,000+ cities. You built a dropdown with all 19,000 options. It takes 8 seconds to load. Users scroll for 30+ seconds trying to find "Springfield" (there are 34 of them).
UX is terrible. Conversion rate on that form: 23%. You lose 77% of people because they can't figure out the location picker. That's 2,300 incomplete records per month. Your sales team calls these leads manually to get the missing location data. Cost: $14,200/month in wasted outreach.
Better solution: Click the field → modal opens → clean two-step picker (State first, then City). Takes 4 seconds instead of 38. Conversion jumps to 81%. Here's how to build it in Odoo 18 without breaking your form view.
You Need This If:
What You're Building
A click-triggered modal that captures complex input and transfers data back to the original field. No page navigation. No form submission. Just clean, inline interaction.
User Flow:
- User clicks the "Location" input field
- Modal pops up with State dropdown + City dropdown
- User selects "California" → City dropdown updates with CA cities only
- User selects "San Diego"
- Clicks "Submit" button in modal
- Modal closes
- Location field now shows: "California, San Diego"
Step 1: Create the QWeb Template (Modal HTML)
First, define what the modal looks like. This is a QWeb template that stays hidden until triggered.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="location_modal_template" name="Location Selector Modal">
<div id="location_temp" class="modal" style="display:none;">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">Select Location</h4>
<button type="button" class="btn-close" id="dismiss"
aria-label="Close"></button>
</div>
<!-- Modal Body -->
<div class="modal-body">
<div class="row">
<!-- State Selector -->
<div class="col-md-6">
<label for="state_id" class="form-label">State</label>
<select id="state_id" class="form-select">
<option value="">-- Select State --</option>
<t t-foreach="states" t-as="state">
<option t-att-value="state['id']"
t-esc="state['name']"/>
</t>
</select>
</div>
<!-- City Selector -->
<div class="col-md-6">
<label for="city_id" class="form-label">City</label>
<input type="text" id="city_id"
class="form-control"
placeholder="Enter city name"/>
</div>
</div>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
id="dismiss">Cancel</button>
<button type="button" class="btn btn-primary"
id="submit_location">Submit</button>
</div>
</div>
</div>
</div>
</template>
</odoo>
Important: The modal starts with style="display:none;". It's hidden until JavaScript triggers it. Don't skip this or you'll have a modal permanently visible on page load.
Step 2: Create the Controller (Pass Data to Template)
Your modal needs data (like the list of states). Create a controller to fetch it.
from odoo import http
from odoo.http import request
class LocationController(http.Controller):
@http.route('/location/form', type='http', auth='public', website=True)
def location_form(self, **kwargs):
# Fetch states from res.country.state model
states = request.env['res.country.state'].sudo().search([
('country_id.code', '=', 'US') # Only US states
])
state_list = [{
'id': state.id,
'name': state.name,
'code': state.code
} for state in states]
return request.render('your_module.location_modal_template', {
'states': state_list
})
This controller queries Odoo's res.country.state model, filters for US states, and passes them to the template as states variable.
Step 3: Write the JavaScript (The Trigger Logic)
Now for the important part: JavaScript that opens/closes the modal and transfers data.
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.LocationModal = publicWidget.Widget.extend({
selector: '#whole_sub', // Parent container
events: {
'click #location_id': '_onLocationClick',
'click #submit_location': '_onSubmitClick',
'click #dismiss': '_onCloseClick',
},
/**
* Open modal when Location field is clicked
*/
_onLocationClick(ev) {
const modal = this.el.querySelector('#location_temp');
modal.style.display = 'block';
},
/**
* Close modal and transfer data to Location field
*/
_onSubmitClick(ev) {
const stateSelect = this.el.querySelector('#state_id');
const cityInput = this.el.querySelector('#city_id');
const locationField = this.el.querySelector('#location_id');
// Get selected state name (not ID)
const stateName = stateSelect.selectedOptions[0].text;
const cityName = cityInput.value;
// Validate inputs
if (!stateName || stateName === '-- Select State --') {
alert('Please select a state');
return;
}
if (!cityName) {
alert('Please enter a city');
return;
}
// Update location field with formatted value
locationField.value = `${stateName}, ${cityName}`;
// Close modal
this._closeModal();
},
/**
* Close modal without saving
*/
_onCloseClick(ev) {
this._closeModal();
},
/**
* Helper: Hide modal
*/
_closeModal() {
const modal = this.el.querySelector('#location_temp');
modal.style.display = 'none';
}
});
What This Code Does
| Function | Trigger | Action |
|---|---|---|
_onLocationClick |
User clicks Location field | Sets modal display to 'block' (shows it) |
_onSubmitClick |
User clicks Submit button | Validates input, formats as "State, City", updates field, closes modal |
_onCloseClick |
User clicks Cancel or X | Closes modal without saving |
Step 4: Register Your Assets
Tell Odoo to load your JavaScript file. Add this to your module's __manifest__.py:
'assets': {
'web.assets_frontend': [
'your_module/static/src/js/location_modal.js',
],
},
Place your JavaScript file at: your_module/static/src/js/location_modal.js
Common Mistakes That Break Modals
1. Forgetting display:none on Modal
Modal shows immediately on page load. Users see it before clicking anything.
Fix: Add style="display:none;" to modal div.
2. Not Validating Input Before Closing
User clicks Submit with empty fields. Modal closes. Field shows "undefined, undefined".
Fix: Check if stateName and cityName exist before updating field.
3. Using state.id Instead of state.name
Location field shows: "42, San Diego" instead of "California, San Diego".
Fix: Use selectedOptions[0].text to get the visible state name, not the value.
4. Multiple Click Bindings
User clicks Location field. Modal opens twice stacked on top of each other.
Fix: Use once: true in event listener or check if modal already visible before opening.
Advanced: Dynamic City Filtering
Want the City dropdown to update based on selected State? Add AJAX call.
import { jsonrpc } from "@web/core/network/rpc_service";
_onStateChange(ev) {
const stateId = ev.target.value;
if (!stateId) return;
// Fetch cities for selected state
jsonrpc('/location/get_cities', {
state_id: stateId
}).then(cities => {
const citySelect = this.el.querySelector('#city_id');
// Clear existing options
citySelect.innerHTML = '<option value="">-- Select City --</option>';
// Add new options
cities.forEach(city => {
const option = document.createElement('option');
option.value = city.id;
option.textContent = city.name;
citySelect.appendChild(option);
});
});
}
Add corresponding controller route to fetch cities by state:
@http.route('/location/get_cities', type='json', auth='public')
def get_cities(self, state_id, **kwargs):
# Fetch cities for the given state
# (Assuming you have a city model)
cities = request.env['res.city'].sudo().search([
('state_id', '=', int(state_id))
])
return [{
'id': city.id,
'name': city.name
} for city in cities]
Real-World Use Cases
1. Multi-Product Configurator
Click "Configure" field → Modal with product options, add-ons, quantities → Submit → Field shows: "Base ($299) + Warranty ($49) + Setup ($75)"
Benefit: Users configure complex products inline without leaving order form.
2. Date Range Picker
Click "Campaign Duration" field → Modal with calendar for start + end dates → Submit → Field shows: "Jan 15, 2025 - Feb 28, 2025"
Benefit: Better than cramming two date pickers in form layout.
3. Multi-Contact Selector
Click "Meeting Attendees" field → Modal with searchable contact list + checkboxes → Submit → Field shows: "John Smith, Sarah Lee, Mike Johnson (3 attendees)"
Benefit: Cleaner than many2many tags widget for large contact lists.
Testing Checklist
- Click trigger works: Modal appears when field is clicked
- Modal displays correctly: No layout breaks, all fields visible
- Validation works: Submit fails if required fields empty
- Data transfers: Field updates with correct formatted value
- Modal closes: Both Submit and Cancel properly hide modal
- No duplicates: Clicking field multiple times doesn't create stacked modals
- Mobile responsive: Modal renders properly on mobile screens
- Keyboard navigation: Tab key moves between fields, Escape closes modal
Performance Considerations
Don't Load Heavy Data on Page Load
If your modal needs 10,000 records, don't fetch them all when page loads.
Bad:
- Controller fetches all 10K records
- Template renders 10K options in dropdown
- Page load: 8.3 seconds
Good:
- Modal opens with empty dropdowns
- User types in search field
- AJAX call fetches matching records only
- Dropdown shows 20 results max
- Page load: 0.9 seconds
Quick Implementation Guide
- Create QWeb template with modal HTML structure (
display:noneby default) - Create controller route to render template with data
- Write JavaScript widget with click handlers for open/close/submit
- Register JS in manifest under
web.assets_frontend - Update module and restart Odoo
- Test click → modal → submit → field update flow
Time Investment: First modal: 2-3 hours. Second modal: 30 minutes. You're reusing the pattern. After 3-4 modals, you'll have a reusable widget you just clone for new fields.
Need Custom Odoo UI Development?
We build advanced Odoo interfaces: modal workflows, dynamic forms, custom widgets, and complex UX patterns. Stop fighting with standard Odoo widgets that don't fit your process.
