OWL
inputs
Record Autocomplete
Odoo 19 OWL component — Record Autocomplete (core)
Live preview
Interactive
Source excerpt
web/static/src/core/record_selectors/record_autocomplete.js
import { Component } from "@odoo/owl";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { _t } from "@web/core/l10n/translation";
import { Domain } from "@web/core/domain";
import { registry } from "@web/core/registry";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
const SEARCH_LIMIT = 7;
const SEARCH_MORE_LIMIT = 320;
export class RecordAutocomplete extends Component {
static props = {
resModel: String,
update: Function,
multiSelect: Boolean,
getIds: Function,
value: String,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
className: { type: String, optional: true },
fieldString: { type: String, optional: true },
placeholder: { type: String, optional: true },
slots: { optional: true },
};
static components = { AutoComplete };
static template = "web.RecordAutocomplete";
setup() {
this.orm = useService("orm");
this.nameService = useService("name");
this.addDialog = useOwnedDialogs();
this.sources = [
{
placeholder: _t("Loading..."),
options: this.loadOptionsSource.bind(this),
optionSlot: this.props.slots?.autoCompleteItem ? "option" : undefined,
},
];
}
addNames(options) {
const displayNames = Object.fromEntries(options);
this.nameService.addDisplayNames(this.props.resModel, displayNames);
}
getIds() {
return this.props.getIds();
}
async loadOptionsSource(name) {
if (this.lastProm) {
this.lastProm.abort(false);
}
this.lastProm = this.search(name, SEARCH_LIMIT + 1);
const nameGets = (await this.lastProm).map(([id, label]) => [
id,
label ? label.split("\n")[0] : _t("Unnamed"),
]);
this.addNames(nameGets);
const options = nameGets.map(([id, label]) => ({
data: {
record: { id, display_name: label },
},
label,
onSelect: () => this.props.update([id]),
}));
if (SEARCH_LIMIT < nameGets.length) {
options.push({
cssClass: "o_m2o_dropdown_option",
label: _t("Search More..."),
onSelect: this.onSearchMore.bind(this, name),
});
}
if (options.length === 0) {
options.push({ label: _t("(no result)") });
}
return options;
}
async onSearchMore(name) {
const { fieldString, multiSelect, resModel } = this.props;
let operator;
const ids = [];
if (name) {
const nameGets = await this.search(name, SEARCH_MORE_LIMIT);
this.addNames(nameGets);
operator = "in";
ids.push(...nameGets.map((nameGet) => nameGet[0]));
} else {
operator = "not in";
ids.push(...this.getIds());
}
const dynamicFilters = ids.length
? [
{
description: _t("Quick search: %s", name),
domain: [["id", operator, ids]],
},
]
: undefined;
// fine for now but we don't like this kind of dependence of core to views
const SelectCreateDialog = registry.category("dialogs").get("select_create");
let title = _t("Search");
if (fieldString && fieldString.trim()) {
title = _t("Search: %s", fieldString);
}
this.addDialog(SelectCreateDialog, {
title,
dynamicFilters,
domain: this.getDomain(),
resModel,
noCreate: true,
multiSelect,
context: this.props.context || {},
onSelected: (resId) => {
const resIds = Array.isArray(resId) ? resId : [resId];
this.props.update([...resIds]);
},
});
}
Registry / API
- Registry name
RecordAutocomplete- Category
—- Module
web- Slug
record-autocomplete- Nav group
inputs