kaystrobach / visualsearch
package to handle advanced facetted searches
Package info
github.com/kaystrobach/FLOW.VisualSearch
Type:neos-package
pkg:composer/kaystrobach/visualsearch
Requires
- neos/flow: ~7.0 || ^8.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 3.18.0
- 3.17.0
- 3.16.2
- 3.16.1
- 3.16.0
- 3.15.0
- 3.14.0
- 3.13.0
- 3.12.1
- 3.12.0
- 3.11.0
- 3.10.0
- 3.9.0
- 3.8.0
- 3.7.0
- 3.6.0
- 3.5.2
- 3.5.1
- 3.5.0
- 3.4.1
- 3.4.0
- 3.3.0
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.0
- 2.x-dev
- 2.4.0
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.6.x-dev
- 1.6.5
- 1.6.4
- 1.6.3
- 1.6.2
- 1.6.1
- 1.6.0
- 1.5.0
- 1.4.0
- 1.3.0
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- 0.5.x-dev
- 0.5.1
- 0.5.0
- 0.4.1
- 0.4.0
- dev-analysis-pE2Kro
- dev-analysis-lZvYad
This package is auto-updated.
Last update: 2026-10-13 17:51:39 UTC
README
This package provides a powerful search component for Flow ecosystem. Any search repository can be made searchable by
extending SearchableRepository and providing an appropriate configuration. The front-end component is provided as a
Fluid partial.
Installation
composer require kaystrobach/visualsearch:^3.0.0
Usage
To make a repository searchable, extend SearchableRepository or implement the SearchableRepositoryInterface.
Note that the defaultSearchName property should set to the name of the corresponding search configuration.
<?php namespace Poke\Search\Domain\Repository; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\Repository; use KayStrobach\VisualSearch\Domain\Repository\SearchableRepository; /** * @Flow\Scope("singleton") */ class PokemonRepository extends SearchableRepository { /** * @var string */ protected $defaultSearchName = 'pokemon'; public function findByName(string $name) { ... } }
The repository can now be queried using the findByDefaultQuery method, e.g., from inside a controller action.
public function indexAction() { $this->view->assign('pokemon', $this->pokemonRepository->findByDefaultQuery()); }
To actually display the search component, include the search partial in your template.
<f:render partial="Visualsearch/Search" arguments="{searchName:'pokemon', pokemon:pokemon}" contentAs="value"> ... </f:render>
Listening to Events
You can use the visual-search-listener component to react to search events.
<visual-search-listener event="VisualSearchRequestUpdateChanged"> <template> A change has happened, so the search results are not accurate anymore. Hit enter to trigger an update. </template> </visual-search-listener>
Events
The visual-search component fires the following events. All of them are CustomEvents that bubble and are
composed, so they can be caught on the component itself, on any ancestor, or on window.
| Event | Cancelable | Fired when | Effect of preventDefault() |
|---|---|---|---|
change |
yes | The user types into the search input. | Autocomplete is not updated for this input. |
search |
yes | The search is submitted using the search button. | The query is neither stored nor is the page reloaded. |
sorting |
yes | The sort selection changes, either via the sort dropdown or via setSorting() (e.g. from visual-search-sort). |
The query is neither stored nor is the page reloaded. |
VisualSearchRequestUpdate<Property> |
yes | A reactive property of the component changes. <Property> is the property name with the first letter capitalized. |
None, the update is applied anyway. |
debug |
no | Internal log messages are written (also every time one of the events above is fired). | - |
The most useful VisualSearchRequestUpdate<Property> events are:
VisualSearchRequestUpdateSelectedFacets: a facet or value has been added or removed.VisualSearchRequestUpdateChanged: the selected facets differ from the ones the current results are based on.VisualSearchRequestUpdateAutocomplete: the autocomplete suggestions have been updated.
Events are also fired for the other properties (Facets, Values, Query, Sorting, Search, ShowDebugLog,
NestedFacets, FacetsAction, ValueAction, QueryAction).
event.detail.message contains the JSON encoded event name. debug events carry the log message in
event.detail.message instead.
document.querySelector('visual-search').addEventListener('search', (event) => { if (!confirm('Really search?')) { event.preventDefault(); } });
Note that pressing enter in the input as well as the clear button store the query and reload the page without firing
the search event.
Additionally, the internal search-facet elements fire a facet-delete event when the remove button of a facet is
clicked. It bubbles out of the component as well, but carries no information about the removed facet.
Handled Events
The visual-search component does not listen for any events from outside, it can only be controlled via its
attributes (e.g. query) and the setSorting(value) and applyQuery(query) methods. Internally it handles the following events:
| Element | Event | Action |
|---|---|---|
| Search input | focus |
Shows the autocomplete suggestions if the input is empty. |
| Search input | input |
Fires change and updates the autocomplete suggestions. |
| Search input | keydown |
Enter completes the first suggestion or stores the query and reloads, Backspace on an empty input removes the last facet, Tab/ArrowDown/ArrowUp move the focus into the suggestions, Escape blurs the input. |
| Autocomplete suggestion | click |
Adds the suggested facet or value. |
| Autocomplete suggestion | keydown |
ArrowDown/ArrowUp/Tab move between suggestions, Escape returns the focus to the input. |
| Search button | click |
Submits the search (fires search). |
| Sort select | change |
Fires sorting and submits the search. |
| Clear button | click |
Removes all facets and the sorting, stores the query and reloads. |
search-facet |
facet-delete |
Removes the facet. |
The companion components listen for these events:
| Component | Event | Listens on | Action |
|---|---|---|---|
visual-search-listener |
value of event |
The elements matching selector, or else window |
Shows the content of its <template>. |
visual-search-sort |
search, sorting |
window |
Updates its state from the query of the search component. |
visual-search-preset |
click |
Itself | Applies its query to the search component and submits it. |
visual-search-preset |
VisualSearchRequestUpdateSelectedFacets, VisualSearchRequestUpdateQuery, sorting |
The search component | Updates its data-active attribute and the class of the wrapped elements. |
debug-log |
debug |
Its parent node | Appends the message to the log. |
External Sorting
You can use the visual-search-sort component to trigger sorting from outside the main search component.
<visual-search-sort asc="1" desc="2" selector="#my-search"> <span slot="neutral">↕</span> <span slot="asc">▲</span> <span slot="desc">▼</span> </visual-search-sort>
The component supports the following attributes:
asc: The value to set for ascending sort.desc: (Optional) The value to set for descending sort.selector: (Optional) A CSS selector to find thevisual-searchcomponent (defaults tovisual-search).template: (Optional) ID of a<template>tag to use for rendering. If used, the wrapperdivwill have a classstate-neutral,state-asc, orstate-descwhich you can use for styling.
Search Presets
You can use the visual-search-preset component to apply a predefined query to the search component. Wrap it around
any clickable element, e.g. a button or a link.
<visual-search-preset selector="#my-search" query='{"facets": [{"facet": "status", "facetLabel": "Status", "value": "open", "valueLabel": "Open"}], "sorting": "1"}'> <button type="button">Open orders</button> </visual-search-preset>
In Fluid templates, use f:format.json to build the query, as inline JSON clashes with the Fluid syntax. See the
demo template for
more examples.
<visual-search-preset query="{f:format.json(value: {facets: {0: {facet: 'weight', value: '100'}}, sorting: '3010'})}"> <button type="button">Weight 100, tallest first</button> </visual-search-preset>
On click, the selected facets (and the sorting, if given) of the search component are replaced by the query and the search is submitted.
As long as the facets of the search component match the facets of the query, the wrapped elements get the class
active and the preset itself gets a data-active attribute, which you can use for styling. The order of the facets
does not matter. The sorting is only compared if the query contains a sorting key.
button.active { font-weight: bold; }
The preset stays part of the DOM, so selectors like .btn-group > .btn no longer match the wrapped elements. With
Bootstrap button groups, add the class btn-group to the presets, Bootstrap then handles them as nested button groups.
<div class="btn-group" role="group"> <visual-search-preset class="btn-group" query="..."> <button type="button" class="btn btn-outline-primary">Rechnungen</button> </visual-search-preset> <visual-search-preset class="btn-group" query="..."> <button type="button" class="btn btn-outline-primary">Dauerrechnungen</button> </visual-search-preset> </div>
The component supports the following attributes:
query: The query as JSON, in the same format as thequeryof the search component.facetLabelandvalueLabelare optional and default tofacetandvalue.sortinghas to be a key of thesortingconfiguration of the search, otherwise it is ignored (with a warning in the console) and the preset never becomes active.selector: (Optional) A CSS selector to find thevisual-searchcomponent (defaults tovisual-search).active-class: (Optional) The class added to the wrapped elements while the query matches (defaults toactive).match: (Optional)exact(default) requires the search to have exactly the facets of the query.partialonly requires the search to contain all facets of the query, additional facets in the search are allowed. A click always replaces the facets of the search with the facets of the query.
Configuration
Search configurations are defined in the Configuration/VisualSearch.yaml file. Please see the demo package for an
example configuration.
Theming
The search component can be styled using the following CSS properties:
| Custom Property | Default |
|---|---|
| --visual-search-background-color | white |
| --visual-search-color | black |
| --visual-search-background-color-focus | lightgray |
| --visual-search-color-focus | black |
| --visual-search-facet-background-color | lightgray |
| --visual-search-facet-color | black |
Testing with Playwright
Tests/Playwright/visual-search.ts provides VisualSearch, a Playwright component object for the <visual-search>
component. Suggestions are loaded asynchronously and an older request may overwrite the suggestions for the typed term,
the query is stored in the session and survives page loads. The helper waits for the events of the component
(VisualSearchRequestUpdateAutocomplete, VisualSearchRequestUpdateSelectedFacets) and for the reload on submit,
so tests do not need timeouts.
The helper imports @playwright/test, which has to be installed in the project running the tests. Map the directory
in the tsconfig.json of that project, the path depends on where the package is installed:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@kaystrobach/visualsearch/*": ["Packages/Application/KayStrobach.VisualSearch/Tests/Playwright/*"]
}
}
}
Create it with new VisualSearch(page), or pass the locator of the component as second argument if a page contains
more than one search.
import { expect, test } from "@playwright/test"; import { VisualSearch } from "@kaystrobach/visualsearch/visual-search"; test("search", async ({ page }) => { const visualSearch = new VisualSearch(page); // free text search: removes the stored query, adds 'Text "pikachu"' and submits await visualSearch.search("pikachu"); // facet search await visualSearch.clear(); await visualSearch.addFacet("Identifier", "pikachu"); await expect(visualSearch.facet("Identifier")).toHaveAttribute("value-label", "pikachu"); await visualSearch.submit(); });
| Method | Purpose |
|---|---|
focus() |
focus the input and wait for the initial suggestions |
clear() |
remove all facets of the stored query |
type(term, expectedSuggestion?) |
type until the expected suggestion (default: term) is shown |
chooseSuggestion(label) |
click a suggestion (facet or value) |
addText(term) |
add a free text facet |
addFacet(facet, value) |
add a facet with a value, without suggestions the typed value is used |
submit() |
store the query and wait for the reloaded page |
search(term) |
clear(), addText(term) and submit() |
withEvent(name, action) |
run an action and wait for an event of the component |
Locators: input, dropdown, suggestions, suggestion(label), facets, facet(label).
A facet keeps its label and value in the attributes facet-label and value-label of <search-facet>.
Development
Install front-end dependencies via npm.
cd Resources/Private/App
npm install
After making changes to the front-end code, run the build script to bundle the assets.
npm run build
Linters are available for both JavaScript and CSS templates.
npm run lint:js npm run lint:css
License
This project is licensed under the MIT License.
