Components

AdvancedSearch

An advanced search and filter composition component.

Usage

AdvancedSearch is a wrapper around the CommandPalette component and, for the most part, uses the same structure. Use the v-model directive to control the checked option.

Loading preview...
<template>
  <MeAdvancedSearch
    :provider="provider"
  >
    <UButton
      color="neutral"
      variant="subtle"
    >
      Open
    </UButton>

    <template #content-top>
      <UButton
        color="neutral"
        variant="outline"
        :ui="{ base: 'rounded-sm' }"
      >
        Action A
      </UButton>

      <UButton
        color="neutral"
        variant="outline"
        :ui="{ base: 'rounded-sm' }"
      >
        Action B
      </UButton>
    </template>

    <template #footer>
      <UButton :ui="{ base: 'rounded-sm' }">
        Footer A
      </UButton>
    </template>
  </MeAdvancedSearch>
</template>

<script setup>
function provider(args) {
  if (!args) {
    return new Promise((resolve) => {
      resolve([{
        id: 'group1',
        label: 'Group 1',
        items: [{
          label: 'Group item 1',
          id: 'group-item-1',
          suffix: 'Group item 1 suffix',
          children: true
        }]
      }, {
        id: 'criterion',
        label: 'Criterion',
        items: [{
          label: 'Name',
          id: 'name',
          suffix: 'Filter by name',
          fieldType: 'string',
          children: [
            {
              id: 'equal',
              operatorKey: 'equal',
              children: [
                { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
                { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
              ]
            },
            {
              id: 'not_equal',
              operatorKey: 'not_equal',
              children: [
                { id: 'document', label: '123456789', suffix: 'Document number' },
                { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
              ]
            }
          ]
        }]
      }])
    })
  }

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
        { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
        { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
      ])
    }, 1000)
  })
}
</script>

Groups

Use the provider prop to define the groups. It must be a function that returns a Promise. When a group describes a criterion, its id must be "criterion" and its children can include the fieldType and operatorKey keys (see CriterionInput). This structure guides the component to correctly construct the criterion as desired by the user. When a criterion is fully created, the criterionSelect event is emitted with the AdvancedSearchCriterion object as payload.

AdvancedSearch also uses internal predefined IDs for its built-in groups that should not be reused as the ID for any group returned by the provider:

  • "criterion"
  • "genius"
  • "genius-items"
  • "recent-search"
  • "recent-search-item"
  • "content-top-slot-group"
  • "empty-slot-group"
  • "empty-slot-item"

If the children field of an AdvancedSearchItem is true, the provider will be called with AdvancedSearchProviderArgs to retrieve an array of children.

type provider = {
  (): Promise<AdvancedSearchGroup<C>[]>
  (args: AdvancedSearchProviderArgs<C>): Promise<AdvancedSearchItem<C>[]>
}

type AdvancedSearchProviderArgs<C extends boolean = false> = {
  groupId: string
  parentItemId?: string
  selectedItem: AdvancedSearchItem<C>
}

type AdvancedSearchGroup<C extends boolean = false> = Omit<CommandPaletteGroup, 'items'> & {
  items: AdvancedSearchItem<C>[]
}

type AdvancedSearchItem<C extends boolean = false> = Omit<CommandPaletteItem, 'children'> & {
  id: string
  children?: true | AdvancedSearchItem<C>[]
} & (C extends true ? {
  fieldType?: MeFieldTypesValues
  operatorKey?: MeOperatorKeysValues
} : Record<never, never>)

type AdvancedSearchCriterion = {
  fieldId: string
  operatorKey: MeOperatorKeysValues
  value?: AdvancedSearchItem<true>
}
<template>
  <MeAdvancedSearch :provider="provider">
    <UButton
      color="neutral"
      variant="subtle"
    >
      Open
    </UButton>
  </MeAdvancedSearch>
</template>

<script setup lang="ts">
function provider(args) {
  if (!args) {
    return new Promise((resolve) => {
      resolve([{
        id: 'group1',
        label: 'Group 1',
        items: [{
          label: 'Group item 1',
          id: 'group-item-1',
          suffix: 'Group item 1 suffix',
          children: true
        }]
      }, {
        id: 'criterion',
        label: 'Criterion',
        items: [{
          label: 'Name',
          id: 'name',
          suffix: 'Filter by name',
          fieldType: 'string',
          children: [
            {
              id: 'equal',
              operatorKey: 'equal',
              children: [
                { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
                { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
              ]
            },
            {
              id: 'not_equal',
              operatorKey: 'not_equal',
              children: [
                { id: '123456789', label: 'Document', suffix: 'Document number' },
                { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
              ]
            }
          ]
        }]
      }])
    })
  }

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
        { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
        { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
      ])
    }, 1000)
  })
}
</script>

Hide groups

Use the hideGroups prop to hide custom or built-in groups.

<template>
  <MeAdvancedSearch
    :provider="provider"
    :hide-groups="['criterion']"
  >
      <UButton
        color="neutral"
        variant="subtle"
      >
        Open
      </UButton>
  </MeAdvancedSearch>
</template>

<script>
function provider(args) {
  if (!args) {
    return new Promise((resolve) => {
      resolve([{
        id: 'group1',
        label: 'Group 1',
        items: [{
          label: 'Group item 1',
          id: 'group-item-1',
          suffix: 'Group item 1 suffix',
          children: true
        }]
      }, {
        id: 'criterion',
        label: 'Criterion',
        items: [{
          label: 'Name',
          id: 'name',
          suffix: 'Filter by name',
          fieldType: 'string',
          children: [
            {
              id: 'equal',
              operatorKey: 'equal',
              children: [
                { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
                { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
              ]
            },
            {
              id: 'not_equal',
              operatorKey: 'not_equal',
              children: [
                { id: '123456789', label: 'Document', suffix: 'Document number' },
                { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
              ]
            }
          ]
        }]
      }])
    })
  }

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
        { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
        { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
      ])
    }, 1000)
  })
}
</script>

Use the placeholder prop and searchTerm model to customize the search.

<template>
  <MeAdvancedSearch
    v-model:search-term="searchTerm"
    :provider="provider"
    placeholder="Search"
  >
    <UButton
      color="neutral"
      variant="subtle"
    >
      Open
    </UButton>
  </MeAdvancedSearch>
</template>

<script setup lang="ts">
const searchTerm = ref('')

function provider(args) {
  if (!args) {
    return new Promise((resolve) => {
      resolve([{
        id: 'group1',
        label: 'Group 1',
        items: [{
          label: 'Group item 1',
          id: 'group-item-1',
          suffix: 'Group item 1 suffix',
          children: true
        }]
      }, {
        id: 'criterion',
        label: 'Criterion',
        items: [{
          label: 'Name',
          id: 'name',
          suffix: 'Filter by name',
          fieldType: 'string',
          children: [
            {
              id: 'equal',
              operatorKey: 'equal',
              children: [
                { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
                { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
              ]
            },
            {
              id: 'not_equal',
              operatorKey: 'not_equal',
              children: [
                { id: '123456789', label: 'Document', suffix: 'Document number' },
                { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
              ]
            }
          ]
        }]
      }])
    })
  }

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
        { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
        { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
      ])
    }, 1000)
  })
}
</script>

Empty

Use the empty prop to customize the empty state when there are no search results and the genius group is hidden. It render a Empty component.

type empty = Pick<EmptyProps, 'title' | 'description' | 'actions'>

/*
title?: string
description?: string
actions?: ButtonProps[]
*/
<template>
  <MeAdvancedSearch
    :provider="provider"
    :hide-groups="['genius']"
    :empty="{
      title: 'No results found.',
      description: 'No results found for the current search.',
      actions: [
        { label: 'Action A', variant: 'solid', color: 'primary' },
        { label: 'Action B', variant: 'subtle', color: 'neutral' }
      ]
    }"
  >
    <UButton
      color="neutral"
      variant="subtle"
    >
      Open
    </UButton>
  </MeAdvancedSearch>
</template>

<script setup lang="ts">
function provider() {
  return new Promise((resolve) => {
    resolve([{
      id: 'group1',
      label: 'Group 1',
      items: [{
        label: 'Group item 1',
        id: 'group-item-1',
        suffix: 'Group item 1 suffix',
        children: [
          { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
          { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
          { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
        ]
      }]
    }, {
      id: 'criterion',
      label: 'Criterion',
      items: [{
        label: 'Name',
        id: 'name',
        suffix: 'Filter by name',
        fieldType: 'string',
        children: [
          {
            id: 'equal',
            operatorKey: 'equal',
            children: [
              { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
              { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
            ]
          },
          {
            id: 'not_equal',
            operatorKey: 'not_equal',
            children: [
              { id: 'document', label: '123456789', suffix: 'Document number' },
              { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
            ]
          }
        ]
      }]
    }])
  })
}
</script>

Custom content

Use the content-top, footer and empty slots to add custom content. The footer slot is scoped with a close function.

<template>
  <MeAdvancedSearch
    :provider="provider"
    :hide-groups="['genius']"
  >
    <UButton
      color="neutral"
      variant="subtle"
    >
      Open
    </UButton>

    <template #empty>
      <div class="w-full h-25 flex items-center justify-center">
        <UButton
          variant="solid"
          color="primary"
        >
          Empty action
        </UButton>
      </div>
    </template>

    <template #content-top>
      <UButton
        color="neutral"
        variant="outline"
        :ui="{ base: 'rounded-sm' }"
      >
        Action A
      </UButton>

      <UButton
        color="neutral"
        variant="outline"
        :ui="{ base: 'rounded-sm' }"
      >
        Action B
      </UButton>
    </template>

    <template #footer="{ close }">
      <UButton
        :ui="{ base: 'rounded-sm' }"
        @click="close"
      >
        Close
      </UButton>
    </template>
  </MeAdvancedSearch>
</template>

<script setup lang="ts">
function provider(args) {
  if (!args) {
    return new Promise((resolve) => {
      resolve([{
        id: 'group1',
        label: 'Group 1',
        items: [{
          label: 'Group item 1',
          id: 'group-item-1',
          suffix: 'Group item 1 suffix',
          children: true
        }]
      }, {
        id: 'criterion',
        label: 'Criterion',
        items: [{
          label: 'Name',
          id: 'name',
          suffix: 'Filter by name',
          fieldType: 'string',
          children: [
            {
              id: 'equal',
              operatorKey: 'equal',
              children: [
                { id: 'john-doe', label: 'John Doe', suffix: 'John Doe username' },
                { id: 'jane-doe', label: 'Jane Doe', suffix: 'Jane Doe username' }
              ]
            },
            {
              id: 'not_equal',
              operatorKey: 'not_equal',
              children: [
                { id: '123456789', label: 'Document', suffix: 'Document number' },
                { id: 'john-doe-email', label: 'john.doe@example.com', suffix: 'Email' }
              ]
            }
          ]
        }]
      }])
    })
  }

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 'item-1', label: 'Item 1', suffix: 'Item 1 suffix' },
        { id: 'item-2', label: 'Item 2', suffix: 'Item 2 suffix' },
        { id: 'item-3', label: 'Item 3', suffix: 'Item 3 suffix' }
      ])
    }, 1000)
  })
}
</script>

API

Props

PropDefaultType
modelValueAdvancedSearchItem
The checked item, bind with v-model
searchTerm''String
The search input value, bind with v-model:search-term
placeholder'Make a direct search or find a filter'String
The search input placeholder
openfalseBoolean
The open state, bind with v-model:open
hideGroups[]String[]
The IDs of the groups to omit
provider(): Promise<AdvancedSearchGroup<C>[]> | (args: AdvancedSearchProviderArgs<C>): Promise<AdvancedSearchItem<C>[]>
empty{ title?: string; description?: string; actions?: ButtonProps[] }

Emits

EventType
searchEnter[string]
searchGenius[string]
searchRecent[AdvancedSearchRecentSearch]
criterionSelect[AdvancedSearchCriterion]

Slots

SlotType
default{}
content-top{}
empty{}
footer{ close: () => void }