Filter BarDraft
<script lang="ts" setup>
import { z } from 'zod'
import { FilterDropdown } from '#components'
const speciesOptions = [
{ label: 'Amphibian', value: 'AMPHIBIAN' },
{ label: 'Canine', value: 'CANINE' },
{ label: 'Feline', value: 'FELINE' },
{ label: 'Rodents', value: 'RODENTS' },
]
type Filter = {
id: string
label: string
value: Ref<string | Array<string>>
options: Array<{ label: string; value: string }>
}
const additionalFilters: Filter[] = [
{
id: 'client',
label: 'Client',
value: ref([]),
options: [
{ label: 'Donald Johnson', value: '17' },
{ label: 'Claudia Olson', value: '32' },
{ label: 'Jesse Wheeler', value: '123' },
],
},
{
id: 'patient',
label: 'Patient',
value: ref([]),
options: [
{ label: 'Pixie', value: '3' },
{ label: 'Nimba', value: '33' },
{ label: 'Bink', value: '77' },
{ label: 'Luna', value: '99' },
],
},
]
function getLabelForAddedFilter(filterId: string) {
return additionalFilters.find((filter) => filter.id === filterId)?.label ?? ''
}
function getOptionsForAddedFilter(filterId: string) {
return (
additionalFilters.find((filter) => filter.id === filterId)?.options ?? []
)
}
function getValueForAddedFilter(filterId: string) {
return additionalFilters.find((filter) => filter.id === filterId)?.value
}
function updateValueForAddedFilter(filterId: string, value: string | string[]) {
const filter = additionalFilters.find((filter) => filter.id === filterId)
if (filter) {
filter.value.value = value
}
}
const addedFilterIds = ref<string[]>([])
const availableFiltersToAdd = computed(() =>
additionalFilters.filter((filter) => {
return !addedFilterIds.value.some(
(addedFilterId) => addedFilterId === filter.id,
)
}),
)
async function addFilter(filterId: string) {
addedFilterIds.value.push(filterId)
await nextTick()
if (addedFilterDropdownRefs.value.length) {
addedFilterDropdownRefs.value[
addedFilterDropdownRefs.value.length - 1
].open()
}
}
function removeFilter(filterId: string) {
addedFilterIds.value = addedFilterIds.value.filter(
(addedFilterId) => addedFilterId !== filterId,
)
}
const addedFilterDropdownRefs = ref<InstanceType<typeof FilterDropdown>[]>([])
const cardElement = ref<HTMLElement>()
const { width } = useElementSize(cardElement)
// based the breakpoint on some manual testing
const isNarrow = computed(() => width.value <= 1090)
const status = ['ACTIVE', 'ARRIVED', 'CONSULTATION', 'DISCHARGED'] as const
type Status = (typeof status)[number]
const statusTranslations = {
ACTIVE: 'Active',
ARRIVED: 'Arrived',
CONSULTATION: 'Consultation',
DISCHARGED: 'Discharged',
}
const statusOptions = Object.entries(statusTranslations).map(
([value, label]) => ({
value,
label,
}),
)
const filterSchema = z.object({
search: z.string().default(''),
statusIn: z.array(z.string()).default([]),
speciesIn: z.array(z.string()).default([]),
clientIn: z.array(z.string()).default([]),
patientIn: z.array(z.string()).default([]),
addedFilters: z.array(z.string()).default([]),
})
const { filters, isInitialFilters, onFiltersChange, onResetFilters } =
useFiltersURLState(filterSchema)
// Search Filter
const search = ref(filters?.value?.search)
const searchDebounced = refDebounced(search)
// Status Filter
const statusIn = ref(filters?.value?.statusIn || [])
// Species Filter
const speciesIn = ref(filters?.value?.speciesIn || [])
// If filters URL state changes, update internal values accordingly
watchDebounced(
() => filters?.value,
(newFilters) => {
search.value = newFilters?.search
statusIn.value = newFilters?.statusIn
speciesIn.value = newFilters?.speciesIn
updateValueForAddedFilter('client', newFilters?.clientIn)
updateValueForAddedFilter('patient', newFilters?.patientIn)
addedFilterIds.value = newFilters?.addedFilters
},
{ deep: true, immediate: true },
)
// If any of the internal values change, update filters URL changes accordingly
watch(
[
searchDebounced,
statusIn,
speciesIn,
getValueForAddedFilter('client'),
getValueForAddedFilter('patient'),
addedFilterIds,
],
() => {
return onFiltersChange({
search: search.value,
statusIn: statusIn.value,
speciesIn: speciesIn.value,
clientIn: getValueForAddedFilter('client')?.value,
patientIn: getValueForAddedFilter('patient')?.value,
addedFilters: addedFilterIds.value,
})
},
{ deep: true, immediate: true },
)
function toggleStatus(toggled: Status) {
const set = new Set(statusIn.value)
if (set.has(toggled)) {
set.delete(toggled)
} else {
set.add(toggled)
}
statusIn.value = [...set]
}
</script>
<template>
<provet-stack direction="horizontal" gap="s" wrap>
<provet-input v-model="search" size="s" label="Search items" hide-label>
<provet-icon
slot="start"
name="navigation-search"
size="xs"
></provet-icon>
</provet-input>
<!-- Render a filter dropdown for the status filter at narrow widths... -->
<FilterDropdown
v-if="isNarrow"
v-model="statusIn"
label="Status"
:options="statusOptions"
multiple
/>
<!-- ...or a tag group if space allows -->
<template v-else>
<provet-visually-hidden id="status-label">
Time frame
</provet-visually-hidden>
<provet-tag-group aria-labelledby="status-label">
<provet-tag
v-for="value in status"
:key="value"
size="s"
variant="selectable"
:checked="statusIn.includes(value)"
@change="toggleStatus(value)"
>
{{ statusTranslations[value] }}
</provet-tag>
</provet-tag-group>
</template>
<FilterDropdown
v-model="speciesIn"
label="Species"
:options="speciesOptions"
multiple
/>
<provet-divider direction="vertical"></provet-divider>
<template v-for="filterId in addedFilterIds" :key="filterId">
<FilterDropdown
ref="addedFilterDropdownRefs"
:model-value="getValueForAddedFilter(filterId)?.value"
:label="getLabelForAddedFilter(filterId)"
:options="getOptionsForAddedFilter(filterId)"
multiple
removable
@remove="removeFilter(filterId)"
@update:model-value="updateValueForAddedFilter(filterId, $event || '')"
/>
</template>
<FilterAddButton
v-if="availableFiltersToAdd.length"
:options="availableFiltersToAdd"
@select="addFilter($event)"
/>
<FiltersResetButton :disabled="isInitialFilters" @click="onResetFilters" />
</provet-stack>
</template>
Integration
This product pattern is currently only available to use in the New Frontend for Provet Cloud (using Vue & Nuxt).
Troubleshooting
If you experience any issues while using this pattern, please ask for support in the #vet-frontend Slack channel.