Skip to Content
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