Search
The Search widget is a search box that remembers its term for the session and fires search.submit when the term changes. A toolbar builds one from its search option and binds it to a list. Ui::searchInput() renders the same box for any AJAX handler.
Search widget on its own
Made with Search::make(), the widget runs its own onSubmit handler as you type, saves the term in the session and fires search.submit. Whatever the listeners return goes back as the response, here an update of the tag results. getActiveTerm() reads the saved term, so the page renders the same results on its first load, and the term is still in the box after a reload. growable: false keeps the box at its full width.
/**
* makeTagSearch is a search box on its own, which updates the tag results through its search.submit event.
*/
protected function makeTagSearch(): Search
{
$search = Search::make(
alias: 'tagSearch',
prompt: 'Search tags...',
growable: false,
);
$search->bindEvent('search.submit', function () use ($search) {
return ['#tagResults' => $this->renderTagResults($search->getActiveTerm())];
});
return $search;
}
/**
* renderTagResults renders the tags whose name contains the term.
*/
protected function renderTagResults(string $term): string
{
return view('reference.lists.search._tag_results', [
'tags' => Tag::where('name', 'like', "%{$term}%")->withCount('posts')->orderBy('name')->get(),
'term' => $term,
])->render();
}@if ($tags->isNotEmpty())
<ul class="list-unstyled d-flex flex-wrap gap-2 mb-0">
@foreach ($tags as $tag)
<li class="badge badge-subtle theme-secondary">
{{ $tag->name }}
<span class="fg-3">
{{ $tag->posts_count }}
</span>
</li>
@endforeach
</ul>
@else
<p class="fg-3 mb-0">
No tags match "{{ $term }}".
</p>
@endifBound to a list
A toolbar's search box searches the list the toolbar is bound to with bindToListWidget(). The list looks in its columns marked searchable, including columns read from a relation, and every word of the term must match, each in any of those columns. A new term takes the list back to its first page. The box grows while it has focus, and its clear button empties it.
Searching works on plain Eloquent models as well as October Rain models, since Larajax UI adds the searchWhere() query methods it uses to Eloquent as builder macros.
/**
* makeSearchedList builds a list of posts for a toolbar search to search.
*/
protected function makeSearchedList(string $alias): Lists
{
return Lists::make(
alias: $alias,
model: new Post,
columns: '~/resources/ui/reference/lists/search/posts.yaml',
recordsPerPage: 4,
noRecordsMessage: 'No posts match the search.',
customPageName: false,
);
}
/**
* makeBoundToolbar searches the given list as you type.
*/
protected function makeBoundToolbar(Lists $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'boundToolbar',
search: ['prompt' => 'Search posts...'],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}columns:
title:
label: Title
searchable: true
excerpt:
label: Excerpt
type: summary
searchable: true
category:
label: Category
relation: category
select: nameSearch scope
scope names a model scope that runs the search in place of the searchable columns. It receives the term and the searchable columns, so it can search those and more. The post model's scopeApplySearch() also matches category and tag names, so "frontend" finds the posts in the Frontend category and "javascript" the posts with that tag.
/**
* makeScopeToolbar searches the given list through the post model's applySearch scope.
*/
protected function makeScopeToolbar(Lists $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'scopeToolbar',
search: [
'prompt' => 'Search posts, categories and tags...',
'scope' => 'applySearch',
],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}/**
* scopeApplySearch is the list search scope, matching any word in the searchable columns, the category name or a tag name, or the term in the author name.
*
* @param array<int, mixed> $columns
*/
public function scopeApplySearch($query, string $term, array $columns)
{
return $query->where(function ($query) use ($term, $columns) {
$query
->searchWhere($term, $columns, 'any')
->orWhereHas('category', function ($query) use ($term) {
$query->searchWhere($term, ['name'], 'any');
})
->orWhereHas('tags', function ($query) use ($term) {
$query->searchWhere($term, ['name'], 'any');
})
->orWhereHas('author', function ($query) use ($term) {
$query->searchWhere($term, ['name'], 'any');
});
});
}Search mode
mode decides how the words match: all, the default, needs every word, any needs one of them, and exact looks for the whole phrase. Try "dark lists" here, which finds the posts with either word.
/**
* makeModeToolbar finds the posts matching any of the words.
*/
protected function makeModeToolbar(Lists $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'modeToolbar',
search: [
'prompt' => 'Search any word...',
'mode' => 'any',
],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}Search on Enter
searchOnEnter waits for Enter before searching, for searches that are slow to run. growable: false keeps the box at one width. With partial, the widget renders a partial of the controller in place of its own box.
/**
* makeEnterToolbar searches when Enter is pressed, in a box that keeps its width.
*/
protected function makeEnterToolbar(Lists $list): Toolbar
{
$toolbar = Toolbar::make(
alias: 'enterToolbar',
search: [
'prompt' => 'Type, then press Enter...',
'searchOnEnter' => true,
'growable' => false,
],
);
$toolbar->bindToListWidget($list);
return $toolbar;
}Ui::searchInput
Ui::searchInput() renders the search box on its own, for any page. The search-input control gives it a clear button, which shows once there is text. name, value and placeholder go on the input, along with any other named argument, while class goes on the field around the input and outerClass on the element around that.
<div class="ui-narrow">
{{ Ui::searchInput(name: 'plain_search', placeholder: 'Type to show the clear button...') }}
</div>Ui::searchInput with a handler
With a handler, the input runs that AJAX handler half a second after typing stops, posting the term under the input's name, and shows a loading indicator while it runs. Clearing the box runs the handler again. Here the handler filters a list of cities and updates the results partial.
<div class="ui-narrow mb-4">
{{ Ui::searchInput(name: 'city_search', placeholder: 'Search cities...', handler: 'onSearchCities') }}
</div>
<div id="cityResults">
@include('reference.lists.search._city_results', ['cities' => [], 'term' => ''])
</div>@if ($term === '')
<p class="fg-3 mb-0">
Type a city name, such as Paris or Sydney.
</p>
@elseif ($cities)
<ul class="list-unstyled d-flex flex-wrap gap-2 mb-0">
@foreach ($cities as $city)
<li class="badge">
{{ $city }}
</li>
@endforeach
</ul>
@else
<p class="fg-3 mb-0">
No cities match "{{ $term }}".
</p>
@endif/**
* onSearchCities filters the city list by the search input and updates the results.
*/
public function onSearchCities(): AjaxResponse
{
$term = trim((string) input('city_search'));
$cities = collect($this->getCityNames())
->filter(fn (string $city) => $term === '' || str_contains(mb_strtolower($city), mb_strtolower($term)))
->values()
->all();
return ajax()->update([
'#cityResults' => view('reference.lists.search._city_results', [
'cities' => $cities,
'term' => $term,
]),
]);
}
/**
* getCityNames returns the cities the search input filters.
*
* @return array<int, string>
*/
protected function getCityNames(): array
{
return [
'Amsterdam', 'Auckland', 'Barcelona', 'Berlin', 'Brisbane', 'Buenos Aires', 'Cape Town', 'Lisbon',
'Melbourne', 'Montreal', 'Osaka', 'Paris', 'Perth', 'Seoul', 'Sydney', 'Toronto', 'Vancouver',
];
}Search options
Search::make() takes these options as named arguments, and a toolbar's search array takes them as keys. The term is kept in the session until the box is cleared.
| Option | Description |
|---|---|
| prompt | the placeholder text. |
| mode | how the words match: all, any or exact. Passed to the bound list. Default: all. |
| scope | a model scope that runs the search, receiving the term and the searchable columns. Passed to the bound list. |
| searchOnEnter | searches when Enter is pressed, in place of as you type. Default: false. |
| growable | widens the box while it has focus. Default: true. |
| partial | a partial of the controller to render in place of the box. |
| alias | the widget's name, unique on the page. A toolbar names its search box after itself, such as searchToolbarSearch. |
Ui::searchInput arguments
| Argument | Description |
|---|---|
| name | the input name, which the term is posted under. |
| value | the starting value. |
| placeholder | the placeholder text, which is also the input's label for screen readers. Default label: Search. |
| handler | an AJAX handler to run as you type, half a second after typing stops. |
| class | classes for the field around the input. |
| outerClass | classes for the outer element. |
Any other named argument becomes an attribute of the input, with its name in kebab case, so dataRequestFlash: true adds data-request-flash.