Selectors and Descriptors

EMPS ships two built-in Vue components for working with references to other database rows (foreign keys): <selector> — a search-as-you-type picker used in forms — and <descriptor> — a read-only label that resolves an id into a human-readable name. Together they save you from writing a custom autocomplete and a custom "look up the name for this id" query every time a table references another one.


What <selector> Actually Does

<selector> is fundamentally one thing: a search-as-you-type picker that calls into modules/pick/ng (covered below) for its results. There isn't a separate built-in "enum mode" — type always resolves to a pick/ng case.

When type refers to a database table name and :search="true" is set, <selector> queries that table live as the user types:

<selector type="remed"
          v-model="row.remedy_id"
          title="Remedy"
          :search="true"
          :has-extra="false"
          placeholder="Remedy">
</selector>
  • type — the table name (unprefixed, without c_/temp_).
  • v-model — bound to the foreign key column (an id, 0 meaning "none selected").
  • :search="true" — switches the component into live-search-against-a-table mode.
  • title / placeholder — labels shown in the picker UI.
  • :has-extra="false" — disables an optional secondary field some pickers support; leave it false unless you know you need it.

<selector> Is Not for Enums

For a status/type field backed by enum.nn.txt, use a plain <select> instead:

<select v-model="row.status">
    <option v-for="item in enums['order_status']" :key="item.code" :value="item.code">
        {{ item.value }}
    </option>
</select>

See Enum Values for loading enums into a component and the enum_val() display helper. Reach for <selector> only when you're picking an actual database row by id.

<descriptor> is the read-only counterpart — given just an id, it resolves and displays the row's label:

<descriptor type="remed" :value="row.remedy_id" :plain="true"></descriptor>

Both components need to be included once per page, inside the vue_out capture buffer described in Vue.js in EMPS:

{{capture append="vue_out"}}
    ...
    {{include file="db:_comp/selector"}}
    {{include file="db:_comp/descriptor"}}
{{/capture}}


How Entity Search Mode Works: modules/pick/ng

The type="table_name" :search="true" combination is backed by a small project module conventionally located at modules/pick/ng/:

modules/pick/ng/
  list/
    project.php       ← backs <selector> — search + paginated results
  describe/
    project.php       ← backs <descriptor> — resolve a single id to a label

Both files define a local subclass of an EMPS core class:

  • list/project.php defines EMPS_Local_PickList extends EMPS_NG_PickList
  • describe/project.php defines EMPS_Local_PickDescribe extends EMPS_NG_PickDescribe

By default, the parent classes (EMPS_NG_PickList / EMPS_NG_PickDescribe) search and display a column literally called name. If the table you're pointing type at has a name column, everything works with zero extra code — that's the whole point of the convention described on Table Column Naming Conventions.


When the Default Doesn't Work

If the table's main text column is called something else — title, short

  • fullname, or anything other than name — the default query silently returns nothing (or an empty label for <descriptor>). You'll notice this as "I can't find X in the picker" even though the rows clearly exist.

The fix is to add a per-table case to both files, following the same pattern already used for tables that don't have a name column. Here's a real example — a books table whose title lives in a title column, added alongside an existing remed case (which uses short/fullname):

modules/pick/ng/list/project.php — search + row display:

class EMPS_Local_PickList extends EMPS_NG_PickList {
    public function handle_row($ra)
    {
        global $crm, $emps, $bk, $dom;

        if ($this->table_name == "remed") {
            $ra['display_name'] = $crm->captialize($ra['short']) . " / " . $crm->captialize($ra['fullname']);
        } else if ($this->table_name == "books") {
            $ra['display_name'] = $ra['title'];
        } else {
            return parent::handle_row($ra);
        }

        return $ra;
    }

    public function handle_request() {
        global $emps, $start, $perpage, $bk, $key;

        $this->parse_request();

        if (!$emps->auth->credentials("admin")) {
            return;
        }

        if ($this->table_name == "books") {
            $text = $emps->db->sql_escape($emps->utf8_urldecode($_GET['text']));
            $and = $this->make_and($this->filter);
            $perpage = $this->perpage;
            $start = intval($start);

            $r = $emps->db->query("select SQL_CALC_FOUND_ROWS t.* from " .
                TP . $this->table_name . " as t
                where t.title like '%{$text}%' " . $this->where . " " . $and . "
                group by t.id
                order by t.title asc
                limit $start, $perpage");

            $pages = $emps->count_pages($emps->db->found_rows());

            $lst = array();
            while ($ra = $emps->db->fetch_named($r)) {
                $ra = $emps->db->row_types($this->table_name, $ra);
                $ra = $this->handle_row($ra);
                $lst[] = $ra;
            }
        } else {
            parent::handle_request(); exit;
        }

        $response = ['code' => 'OK', 'list' => $lst];
        if ($pages) { $response['pages'] = $pages; }
        echo json_encode($response);
    }
}

modules/pick/ng/describe/project.php — single-id lookup for <descriptor>:

class EMPS_Local_PickDescribe extends EMPS_NG_PickDescribe {
    public function handle_request()
    {
        global $emps, $crm, $dom;
        $this->parse_request();

        if ($this->table_name == "books") {
            $row = $emps->db->get_row("books", "id = {$this->id}");
            $row = $emps->db->row_types("books", $row);
            $row['display_name'] = $row['title'];
        } else {
            parent::handle_request();
            exit;
        }

        echo json_encode(['code' => 'OK', 'display' => $row['display_name']]);
    }
}

Note both files already exist per project (they're where a project's earlier custom cases, like remed, live) — you're adding an else if branch, not creating the module from scratch.


See Also