useFilter
Composable for defining a named filter group whose state can be read and mutated from any component on the page — picker in the header, bar in the sidebar, anywhere.
Mental model:
- A group has an
idand contains one or more filters. - Each filter exposes a
v-model-compatible bind bag —v-bind="filters.<name>"onto any component. - Helpers (
clear,isActive,value) live underfilters.$.<name>. - A reactive
$activelist drives "selected chips with unselect" UIs. { url: true }lazily wires useUrlSync — the dependency is only loaded when the flag is set.
Live Demo
Pickers — placed anywhere
Filter bar — reads $active, calls clear per chip
The { url: true } flag is intentionally off here — its dependency on Nuxt's useRoute only matters when set. In a Nuxt app, pass it as the third argument and the URL will update reactively.
Defining a group
const filters = useFilter("appointments", {
category: { value: null },
status: { value: [] as string[] },
dateRange: { value: { from: null, to: null } },
});Mirror state to the URL
Pass { url: true } as the third argument:
const filters = useFilter("appointments", config, { url: true });useUrlSync is dynamically imported only when this flag is set — so useFilter works in non-Nuxt environments (tests, Storybook, VitePress demos) as long as url stays off. Filter names become top-level URL keys; avoid colliding names across multiple URL-synced groups on the same page.
Retrieving the same group
Call useFilter(id) with no config — anywhere in the app:
const filters = useFilter("appointments");If the group has not been defined yet, this throws. Define before consumers mount (e.g. at a parent component or layout level).
Binding to components
Each filter is a v-model-compatible bag — spread it onto any component that supports v-model:
<orio-selector v-bind="filters.category" :options="categories" />
<orio-checkbox-group v-bind="filters.status" :options="statuses" />
<orio-date-range-picker v-bind="filters.dateRange" />App data (options, labels, etc.) is passed on the component normally — it is not part of the filter config.
Building a filter bar
Iterate $active — a computed list of every filter whose current value differs from its initial:
<template>
<button v-for="f in filters.$active.value" :key="f.name" @click="f.clear">
{{ f.name }}: {{ f.value }} ×
</button>
<orio-button v-if="filters.$active.value.length" @click="filters.$clearAll">
Clear all
</orio-button>
</template>API reference
useFilter(id, config?, options?)
| Argument | Type | Description |
|---|---|---|
id | string | Stable identifier for the group. |
config | Record<string, { value }> | Map of filter name → initial value. Required on the first call for a given id. |
options | { url?: boolean } | url: true lazy-loads useUrlSync and mirrors $state to URL query params. Off by default. |
Returns a FilterGroup.
FilterGroup shape
filters.<name> // { modelValue, 'onUpdate:modelValue' } — spread with v-bind
filters.$.<name> // { value, initial, isActive, clear }
filters.$active // ComputedRef<{ name, value, clear }[]>
filters.$clearAll() // reset every filter to its initial value
filters.$state // Ref<Record<string, unknown>> — raw reactive statedisposeFilter(id)
Removes a group from the registry. Use in SPAs where filter groups are page-scoped:
import { disposeFilter } from "orio-ui";
onBeforeUnmount(() => disposeFilter("appointments"));Caveats
- Define before consume. Calling
useFilter(id)without config throws if the id is unknown. Define the group at a level that runs before any consumer. - No auto-cleanup. Groups persist for the app lifetime. Call
disposeFilter(id)manually when scoping to a route. - URL sync requires Nuxt.
{ url: true }depends onuseUrlSync, which uses Nuxt's#importsfor SSR-safe pre-population. The dependency is lazy-loaded, so leaving the flag off keepsuseFilterusable outside of Nuxt.
See also
- useUrlSync — underlying URL serialization (composed by
{ url: true }).