Skip to content

Ensemble de Tags - DsfrTags

🌟 Introduction

Le composant DsfrTags permet d'afficher un groupe de tags interactifs et personnalisables. Il est particulièrement utile pour gérer des listes de filtres ou des catégories sélectionnables. Il s'appuie sur le composant DsfrTag et offre la possibilité de suivre l'état des tags sélectionnés via une liaison avec v-model.

🏅 La documentation sur le tag sur le DSFR

La story sur le tag sur le storybook de VueDsfr

📐 Structure

Ce composant affiche une liste de tags sous forme de <ul> et permet d'associer un modèle réactif (v-model) pour suivre les sélections des tags interactifs.

🛠️ Props

NomTypePar défautDescription
tagsDsfrTagProps<T>[][]Liste des tags à afficher.
modelValueT[]undefinedListe des valeurs des tags sélectionnés (si les tags sont sélectionnables).

📡 Événements

NomParamètresDescription
update:modelValueT[]Émis lorsqu'un tag sélectionnable est (dé)sélectionné, mettant à jour la liste des valeurs sélectionnées.

🧩 Slots

(Aucun slot spécifique, chaque tag étant généré automatiquement en fonction de la liste fournie en props.)

📝 Exemples

vue
<script lang="ts" setup>
import { computed, ref } from 'vue'

import type { DsfrTagProps } from '@/components/DsfrTag/DsfrTags.types.ts'
import DsfrTags from '@/components/DsfrTag/DsfrTags.vue'

const tagSet: (DsfrTagProps)[] = [
  {
    label: 'Les fruits',
    selectable: true,
    selected: true,
    value: 'fruit',
  },
  {
    label: 'Les légumes',
    selectable: true,
    value: 'legume',
  },
]

type FruitOrVegetable = 'fruit' | 'legume'

const items: { name: string, type: FruitOrVegetable }[] = [
  {
    name: 'Banane',
    type: 'fruit',
  },
  {
    name: 'Pomme',
    type: 'fruit',
  },
  {
    name: 'Poire',
    type: 'fruit',
  },
  {
    name: 'Courgette',
    type: 'legume',
  },
  {
    name: 'Poivron',
    type: 'legume',
  },
  {
    name: 'Navet',
    type: 'legume',
  },
]

const filters = ref<FruitOrVegetable[]>(['fruit', 'legume'])

const filteredItems = computed(() =>
  items // Get all items
    .filter(item => filters.value.includes(item.type)) // Filter according to filters
    .sort((a, b) => a.name > b.name ? 1 : -1), // Sort alphabetically
)
</script>

<template>
  <div class="max-w-90">
    <div class="fr-mt-2w">
      <DsfrTags
        v-model="filters"
        :tags="tagSet"
      />
    </div>
    <ul>
      <li
        v-for="item of filteredItems"
        :key="item.name"
      >
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<style scoped>
.max-w-90 {
  max-width: 90%;
}
</style>

⚙️ Code source du composant

vue
<script lang="ts" setup generic="T = string">
import { computed } from 'vue'

import VIcon from '../VIcon/VIcon.vue'

import type { DsfrTagProps } from './DsfrTags.types'

const props = withDefaults(defineProps<DsfrTagProps<T>>(), {
  label: undefined,
  link: undefined,
  tagName: 'p',
  icon: undefined,
  disabled: undefined,
})

defineEmits<{
  select: [[unknown, boolean]]
}>()

const isExternalLink = computed(() => typeof props.link === 'string' && props.link.startsWith('http'))
const is = computed(() => {
  return props.link
    ? (isExternalLink.value ? 'a' : 'RouterLink')
    : (((props.disabled && props.tagName === 'p') || props.selectable) ? 'button' : props.tagName)
})
const linkProps = computed(() => {
  return { [isExternalLink.value ? 'href' : 'to']: props.link }
})

const dsfrIcon = computed(() => typeof props.icon === 'string' && props.icon.startsWith('fr-icon-'))
const defaultScale = props.small ? 0.65 : 0.9
const iconProps = computed(() => dsfrIcon.value ? undefined : typeof props.icon === 'string' ? { name: props.icon, scale: defaultScale } : { scale: defaultScale, ...(props.icon ?? {}) })
</script>

<template>
  <component
    :is="is"
    class="fr-tag"
    :disabled="disabled"
    :class="{
      'fr-tag--sm': small,
      [icon as string]: dsfrIcon,
      'fr-tag--icon-left': dsfrIcon,
    }"
    :aria-pressed="selectable ? selected : undefined"
    v-bind="{ ...linkProps, ...$attrs }"
    @click="!disabled && $emit('select', [value, selected])"
  >
    <VIcon
      v-if="props.icon && !dsfrIcon"
      :label="iconOnly ? label : undefined"
      :class="{ 'fr-mr-1v': !iconOnly }"
      v-bind="iconProps"
    />
    <template v-if="!iconOnly">
      {{ label }}
    </template>
    <!-- @slot Slot par défaut pour le contenu du tag -->
    <slot />
  </component>
</template>

<style scoped>
.ov-icon {
  margin-top: 0.1rem;
}

.fr-tag {
  align-items: center;
}

.success {
  color: var(--success);
  background-color: var(--bg-success);
}
.error {
  color: var(--error);
  background-color: var(--bg-error);
}
.warning {
  color: var(--warning);
  background-color: var(--bg-warning);
}
.info {
  color: var(--info);
  background-color: var(--bg-info);
}
</style>
ts
export type DsfrTagProps<T = string> = {
  label?: string
  link?: string
  tagName?: string
  icon?: string
  disabled?: boolean
  small?: boolean
  iconOnly?: boolean
} & ({
  selectable: true
  selected?: boolean
  value?: T
} | {
  selectable?: false
})

export type DsfrTagsProps<T = string> = {
  tags: DsfrTagProps<T>[]
  modelValue?: T[]
}