Typing into a field should offer what you have already recorded: company, location, found via #261

Closed
opened 2026-09-17 15:26:27 +00:00 by tiagoagueda · 1 comment
Owner

Adding an application asks for a company by name, and the box is empty every time. Type
Acme today and Acme Ltd next month and there are two employers, two sets of postings,
two rows in the companies table, and a funnel that quietly counts them apart.

The three fields on the intake form that are typed every single time — company_name,
location and source ("Found via") — offer nothing at all
(applications/forms.py:43,46,57). They are plain CharFields.

This is not only convenience

get_or_create_company matches on name__iexact, and its docstring says exactly what that
is for: "Matching loosely on the way in avoids ending up with 'Acme', 'acme' and 'ACME' as
three separate employers after a few weeks of typing."

Case folding catches the variation it was written for and nothing else. Acme Ltd,
ACME Inc., Acme GmbH and a trailing space with a different spelling all create a new
employer, silently, at the moment somebody is busy doing something else. A suggestion list
is the other half of that defence and the better half, because it works before the
record exists rather than trying to reconcile two afterwards.

"Found via" is the same argument with a shorter list. Everybody has four or five answers —
a job board, a friend, the company's own page — and types them out in full, differently,
forever.

Do it with <datalist> first

This project already does this twice and the pattern is settled: a text input carrying
attrs={"list": "…-suggestions", "autocomplete": "off"} and a <datalist> beside it —
industries on the company form (jobs/forms.py:69) and departments on the contact form
(jobs/forms.py:420). The same shape, with the options coming from this person's own
records
instead of a fixed vocabulary, covers everything asked for here.

Three reasons it should be the first attempt rather than a hand-rolled typeahead:

  • It needs no script. Which is this project's standing rule, and here it costs nothing.
  • The browser brings the accessibility. A combobox done by hand is one of the widgets
    most often got wrong — aria-expanded, aria-activedescendant, a listbox relationship,
    arrow keys, a touch keyboard, and all of it again in RTL. A <datalist> has that from
    the platform, in every language Postulo ships.
  • It is already proven here. app.js builds one dynamically for the labels chips, so
    even the scripted case has prior art.

A live endpoint becomes necessary when somebody has enough companies that rendering them
all is silly. That threshold should be measured rather than assumed — a few hundred
names is a few kilobytes — and when it arrives, core:search and the ninja API are both
already there to build on.

What must not go wrong

  • Suggestions are scoped to the person, and there is a test that says so. This is the
    classic shape of an isolation leak: a suggestion endpoint where typing a hands back
    every company on the instance. Everything here must go through for_user, and if a live
    endpoint is built it belongs in the isolation sweep #232 already asks for.
  • Offering must never become requiring. company_name is free text deliberately: a
    company you have never recorded is exactly what a new application usually is. The field
    stays a text box that happens to suggest — not a select, not a validation error.
  • A near-miss deserves a question, not a merge. If somebody types Acme Ltd while
    Acme exists, the honest answer is to ask which they meant. That is a different change
    from offering a list, it carries the risk of merging two employers that really are
    separate subsidiaries, and it should be decided on its own rather than smuggled in here.

Where it pays

Company, location and "Found via" on the intake form; the same company field on the capture
review, which #179 built keys for because it is worked through forty times in a row, and
which therefore gains the most; contact names where a person is attached to an application.
Industries and departments already have theirs.

Adding an application asks for a company by name, and the box is empty every time. Type `Acme` today and `Acme Ltd` next month and there are two employers, two sets of postings, two rows in the companies table, and a funnel that quietly counts them apart. The three fields on the intake form that are typed *every single time* — `company_name`, `location` and `source` ("Found via") — offer nothing at all (`applications/forms.py:43,46,57`). They are plain `CharField`s. ## This is not only convenience `get_or_create_company` matches on `name__iexact`, and its docstring says exactly what that is for: "Matching loosely on the way in avoids ending up with 'Acme', 'acme' and 'ACME' as three separate employers after a few weeks of typing." Case folding catches the variation it was written for and nothing else. `Acme Ltd`, `ACME Inc.`, `Acme GmbH` and a trailing space with a different spelling all create a new employer, silently, at the moment somebody is busy doing something else. A suggestion list is the other half of that defence and the better half, because it works **before** the record exists rather than trying to reconcile two afterwards. "Found via" is the same argument with a shorter list. Everybody has four or five answers — a job board, a friend, the company's own page — and types them out in full, differently, forever. ## Do it with `<datalist>` first This project already does this twice and the pattern is settled: a text input carrying `attrs={"list": "…-suggestions", "autocomplete": "off"}` and a `<datalist>` beside it — industries on the company form (`jobs/forms.py:69`) and departments on the contact form (`jobs/forms.py:420`). The same shape, with the options coming from *this person's own records* instead of a fixed vocabulary, covers everything asked for here. Three reasons it should be the first attempt rather than a hand-rolled typeahead: - **It needs no script.** Which is this project's standing rule, and here it costs nothing. - **The browser brings the accessibility.** A combobox done by hand is one of the widgets most often got wrong — `aria-expanded`, `aria-activedescendant`, a listbox relationship, arrow keys, a touch keyboard, and all of it again in RTL. A `<datalist>` has that from the platform, in every language Postulo ships. - **It is already proven here.** `app.js` builds one dynamically for the labels chips, so even the scripted case has prior art. A live endpoint becomes necessary when somebody has enough companies that rendering them all is silly. That threshold should be **measured rather than assumed** — a few hundred names is a few kilobytes — and when it arrives, `core:search` and the ninja API are both already there to build on. ## What must not go wrong - **Suggestions are scoped to the person, and there is a test that says so.** This is the classic shape of an isolation leak: a suggestion endpoint where typing `a` hands back every company on the instance. Everything here must go through `for_user`, and if a live endpoint is built it belongs in the isolation sweep #232 already asks for. - **Offering must never become requiring.** `company_name` is free text deliberately: a company you have never recorded is exactly what a new application usually is. The field stays a text box that happens to suggest — not a select, not a validation error. - **A near-miss deserves a question, not a merge.** If somebody types `Acme Ltd` while `Acme` exists, the honest answer is to ask which they meant. That is a *different* change from offering a list, it carries the risk of merging two employers that really are separate subsidiaries, and it should be decided on its own rather than smuggled in here. ## Where it pays Company, location and "Found via" on the intake form; the same company field on the capture review, which #179 built keys for because it is worked through forty times in a row, and which therefore gains the most; contact names where a person is attached to an application. Industries and departments already have theirs.
tiagoagueda added this to the 0.4.0 milestone 2026-09-17 15:26:27 +00:00
Author
Owner

#262 proposes adopting Basecoat as the component vocabulary, which brings a Combobox and a Command component with it. It does not override the decision here: the argument for <datalist> — no script, and the browser supplying aria-expanded, aria-activedescendant, arrow keys, the touch keyboard and RTL — does not stop applying because a library offers the widget. Recorded on both sides so neither is quietly contradicted later.

#262 proposes adopting Basecoat as the component vocabulary, which brings a Combobox and a Command component with it. It does **not** override the decision here: the argument for `<datalist>` — no script, and the browser supplying `aria-expanded`, `aria-activedescendant`, arrow keys, the touch keyboard and RTL — does not stop applying because a library offers the widget. Recorded on both sides so neither is quietly contradicted later.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#261
No description provided.