<smart-search-input>

SmartSearchInput

A search-as-you-type component with debounced AJAX requests, multi-select badge display, keyboard navigation, pre-selected items, and a full JavaScript public API. Point it at any endpoint that returns [{id, name}] and it handles the rest.

Async AJAX Debounced Badge Display Keyboard Nav Public API Pre-select
<script src="/static/resources/js/components/smart_search_input.js"></script>

Live Playground

Search books from the Ice & Fire API. Adjust controls to see every attribute in action.


        

Pre-selected Items

Pass a JSON array to pre-selected to populate badges on load. Perfect for edit forms where you need to show existing selections.

<!-- Edit form: pre-populate existing selections -->
<smart-search-input
  label="Selected Tags"
  placeholder="Search more..."
  search-url="/api/tags/"
  input-name="tag_ids"
  badge-variant="success"
  show-badges
  allow-remove
  pre-selected='[
    {"id":1,"name":"Django"},
    {"id":2,"name":"Python"},
    {"id":3,"name":"REST API"}
  ]'
></smart-search-input>

<!-- Or set programmatically from Django template -->
pre-selected=''

Badge Variants

badge-variant maps to Bootstrap badge classes: bg-{variant}. All standard Bootstrap variants are supported.

secondary Django × Python ×
primary React × Vue ×
success Active ×
warning Pending ×
danger Blocked ×
<!-- secondary (default) -->
<smart-search-input
  badge-variant="secondary"
  ...
></smart-search-input>

<!-- primary -->
<smart-search-input
  badge-variant="primary"
  ...
></smart-search-input>

<!-- success / warning / danger / info -->
<smart-search-input
  badge-variant="success"
  ...
></smart-search-input>

Edit & Remove

allow-remove adds a × button to each badge. allow-edit + edit-url-template adds an Edit link that opens the item's edit page — {id} in the template is replaced with the item's id.

<smart-search-input
  label="Team Members"
  search-url="/api/users/"
  input-name="member_ids"
  badge-variant="info"
  show-badges
  allow-remove
  allow-edit
  edit-url-template="/admin/users/{id}/edit/"
  pre-selected='[{"id":1,"name":"Alice Johnson"}]'
></smart-search-input>

<!-- {id} is replaced with each item's actual id
     Edit link opens: /admin/users/1/edit/ -->

Custom Search Handler

Need custom filtering logic, local data, or a non-standard API? Set data-onsearch to a global function name. Return an array of {id, name} objects — the component renders them exactly as it would AJAX results.

<smart-search-input
  label="Search Skills"
  placeholder="Type to filter..."
  input-name="skill_ids"
  badge-variant="warning"
  show-badges
  allow-remove
  data-onsearch="localSkillSearch"
  min-chars="1"
></smart-search-input>

// JS: return [{id, name}] — sync or async both work
const SKILLS = [
  {id:1,name:"Python"},
  {id:2,name:"JavaScript"},
  {id:3,name:"Django"}, // ...
];

function localSkillSearch(query) {
  return SKILLS.filter(s =>
    s.name.toLowerCase().includes(query.toLowerCase())
  );
}

// Async version also works:
async function localSkillSearch(query) {
  const res = await fetch(`/api/skills?q=${query}`);
  return res.json(); // must return [{id, name}]
}

Attributes

AttributeTypeDescriptionDefault
label string Label displayed above the search input Search Items
placeholder string Placeholder text inside the search input Search and add items...
search-url string AJAX endpoint URL. Component appends ?q=<query> and expects [{id, name}]
input-name string The name attribute of the hidden inputs created for each selected item. Use this to retrieve values server-side. selected_items
min-chars number Minimum characters before a search request fires 2
search-delay number Debounce delay in milliseconds between keystrokes and the request 300
max-results number Maximum number of results shown in the dropdown 10
show-badges boolean Render selected items as visible badge chips below the input false
allow-remove boolean Adds a × button to each badge so users can deselect items false
allow-edit boolean Adds an Edit link to each badge. Requires edit-url-template. false
edit-url-template string URL pattern for the edit link. {id} is replaced with the item's id. Example: /admin/users/{id}/edit/
badge-variant string Bootstrap badge colour. Maps to bg-{variant}. Options: secondary, primary, success, info, warning, danger secondary
pre-selected JSON Array of items to pre-select on load. Format: [{id, name}, ...]. Perfect for edit forms.
no-results-text string Message shown in the dropdown when the API returns an empty array No items found!
error-text string Message shown when the fetch request fails Error searching items
data-onsearch string Name of a global function to call instead of the AJAX fetch. Receives the query string, must return [{id, name}] (sync or async).
data-onselect string Global function called when an item is selected. Args: (item, allSelected)
data-onremove string Global function called when an item badge is removed. Args: (item, allSelected)
data-onerror string Global function called when the fetch request throws. Args: (error)

Events & Hooks

Two custom DOM events are dispatched. Both bubble. You can also use the attribute hooks for simpler one-liner callbacks.

const search = document.querySelector('smart-search-input');

// Fires when a result is clicked and added as a badge
search.addEventListener('itemSelected', (e) => {
  console.log('Item selected:', e.detail.item);
  // e.detail.item       → {id, name, ...}
  // e.detail.allSelected → [{id, name}, ...]
});

// Fires when a badge × button is clicked
search.addEventListener('itemRemoved', (e) => {
  console.log('Item removed:', e.detail.item);
  console.log('Remaining:', e.detail.allSelected);
});

// ── Or use attribute hooks on the element ──
<smart-search-input
  data-onselect="handleSelect"
  data-onremove="handleRemove"
  data-onerror="handleError"
></smart-search-input>

function handleSelect(item, allSelected) {
  console.log('Selected:', item.name, '| Total:', allSelected.length);
}

function handleRemove(item, allSelected) {
  console.log('Removed:', item.name);
}

function handleError(error) {
  console.error('Search failed:', error.message);
}
Eventdetail.itemdetail.allSelectedWhen
itemSelected {id, name, …} [{id, name}, …] User clicks a dropdown result
itemRemoved {id, name, …} [{id, name}, …] User clicks × on a badge

Public API

All methods are available on the element reference. Use them to read or mutate selection state from your own JS.

const el = document.querySelector('smart-search-input');

// ── Read ──────────────────────────────────────

// Returns array of selected item objects
el.getSelectedItems();
// → [{id:"1", name:"Django"}, {id:"2", name:"Python"}]

// Returns array of selected item id strings
el.getSelectedIds();
// → ["1", "2"]

// ── Mutate ────────────────────────────────────

// Remove all selected items (badges + hidden inputs)
el.clearSelected();

// Replace entire selection with new set
el.setSelectedItems([
  {id: 5, name: "React"},
  {id: 6, name: "TypeScript"},
]);

// Add a single item programmatically
el.addSelectedItem({id: 7, name: "Docker"});

// Remove a single item by id
el.removeSelectedItem("5");

// Focus the search input
el.focus();
MethodReturnsDescription
getSelectedItems()ArrayAll selected item objects [{id, name, …}]
getSelectedIds()ArrayAll selected item id strings
clearSelected()voidClears all selections, removes badges and hidden inputs
setSelectedItems(items)voidClears selection then sets to [{id, name}] array
addSelectedItem(item)voidAdds one item {id, name}; no-op if already selected
removeSelectedItem(id)voidRemoves item by id string, dispatches itemRemoved
focus()voidFocuses the inner text input

Response Format

Your endpoint must return a flat JSON array. The component reads item.title then item.name then item.label as the display text — whichever exists first wins. item.description is shown as a subtitle if present.

// ✅ Minimum required
[
  { "id": 1, "name": "Django" },
  { "id": 2, "name": "Python" }
]

// ✅ With subtitle (shown under the title in dropdown)
[
  {
    "id": 1,
    "name": "Alice Johnson",
    "description": "Senior Developer"
  }
]

// ✅ title field also works (checked before name)
[
  { "id": 1, "title": "A Game of Thrones" }
]

// ✅ label field also works (checked last)
[
  { "id": 1, "label": "Option A" }
]

// ❌ NOT supported — must be a flat array, not paginated
{ "results": [...], "count": 100 }