Filter BarDraft

Open in new window
View RTL
<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>
Copy to clipboard

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.


Was this page helpful?

YesNo
Send feedback

We use this feedback to improve our documentation.

 
Edit page