Vue.js

Design System - Naive UI

Utiliser une librairie de composants UI pour créer des interfaces modernes et cohérentes

Dans ce chapitre, on reprend le Show Watchlist du chapitre précédent et on remplace chaque élément HTML brut par son équivalent Naive UI.

Pourquoi utiliser une librairie UI ?

Sans librairie UI, chaque bouton, formulaire ou mise en page doit être conçu et stylisé manuellement. Une librairie comme Naive UI offre :

  • Des composants préconçus (boutons, inputs, cartes, grilles…)
  • Un design cohérent appliqué uniformément
  • Des composants accessibles et testés
  • Un gain de temps pour se concentrer sur la logique métier

Installation et configuration

npm install naive-ui

Enregistrement global dans main.ts — tous les composants sont disponibles partout sans import supplémentaire :

// main.ts
import naive from 'naive-ui'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.use(naive)
app.mount('#app')

Remarque : Dans Naive UI, le v-model des inputs s'écrit v-model:value au lieu de v-model. C'est une particularité de la librairie.

Composants essentiels

NButton

Remplace les <button> HTML. Plusieurs types visuels disponibles.

<template>
  <NButton type="primary" @click="emit('toggle-seen', show)">Basculer</NButton>
</template>

Types disponibles : default, primary, info, success, warning, error.

NInput

Remplace les <input> HTML. Utilise v-model:value au lieu de v-model.

<template>
  <NInput v-model:value="search" placeholder="Rechercher une série..."/>
</template>

NTag

Remplace les <span> de statut. Idéal pour afficher l'état vu/à voir d'une série.

<template>
  <NTag :type="show.seen ? 'success' : 'default'">
    {{ show.seen ? '✓ Vu' : 'À voir' }}
  </NTag>
</template>

NCard

Remplace les <li> ou <div> de carte. Propose des slots #header-extra et #footer.

<template>
  <NCard :title="show.title">
    <template #header-extra>
      <NTag :type="show.seen ? 'success' : 'default'">
        {{ show.seen ? '✓ Vu' : 'À voir' }}
      </NTag>
    </template>
    <p>{{ show.genre }} — {{ show.year }}</p>
    <template #footer>
      <NButton type="primary" @click="emit('toggle-seen', show)">Basculer</NButton>
    </template>
  </NCard>
</template>

NGrid et NGi

Remplace la <ul> / <li> pour une mise en page en grille responsive.

<template>
  <NGrid :cols="3" :x-gap="16" :y-gap="16">
    <NGi v-for="show in shows" :key="show.id">
      <ShowCard :show="show" @toggle-seen="emit('toggle-seen', $event)"/>
    </NGi>
  </NGrid>
</template>

NSpace

Gère l'espacement et l'alignement entre éléments, en remplacement des <div> de layout.

<template>
  <NSpace justify="space-between" align="center">
    <NInput v-model:value="search" placeholder="Rechercher une série..."/>
    <NTag type="success">{{ seenShows.length }} vu(s) sur {{ shows.length }}</NTag>
  </NSpace>
</template>

Exemple complet

Le Show Watchlist du chapitre précédent, avec tous les éléments HTML remplacés par Naive UI.

src/components/ShowCard.component.vue

<template>
  <NCard :title="show.title">
    <template #header-extra>
      <NTag :type="show.seen ? 'success' : 'default'">
        {{ show.seen ? '✓ Vu' : 'À voir' }}
      </NTag>
    </template>
    <p>{{ show.genre }} — {{ show.year }}</p>
    <template #footer>
      <NButton type="primary" @click="emit('toggle-seen', show)">Basculer</NButton>
    </template>
  </NCard>
</template>

<script setup lang="ts">
import type { Show } from '@/types'

interface ShowCardProps {
  show: Show
}

defineProps<ShowCardProps>()

const emit = defineEmits<{
  'toggle-seen': [show: Show]
}>()
</script>

src/components/ShowList.component.vue

<template>
  <NSpace vertical :size="16">
    <slot></slot>
    <p v-if="shows.length === 0">Aucune série pour l'instant.</p>
    <NGrid :cols="3" :x-gap="16" :y-gap="16">
      <NGi v-for="show in shows" :key="show.id">
        <ShowCard :show="show" @toggle-seen="emit('toggle-seen', $event)"/>
      </NGi>
    </NGrid>
  </NSpace>
</template>

<script setup lang="ts">
import type { Show } from '@/types'
import ShowCard from './ShowCard.component.vue'

interface ShowListProps {
  shows: Show[]
}

defineProps<ShowListProps>()

const emit = defineEmits<{
  'toggle-seen': [show: Show]
}>()
</script>

src/pages/HomePage.vue

<template>
  <div>
    <ShowList :shows="shows" @toggle-seen="onToggleSeen">
      <NSpace justify="space-between" align="center">
        <NInput v-model:value="search" placeholder="Rechercher une série..."/>
        <NTag type="success">{{ seenShows.length }} vu(s) sur {{ shows.length }}</NTag>
      </NSpace>
    </ShowList>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { Show } from '@/types'
import ShowList from '../components/ShowList.component.vue'

const search = ref<string>('')

const shows = ref<Show[]>([
  { id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true },
  { id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false },
  { id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false },
  { id: 4, title: 'Parasite', genre: 'Thriller', year: 2019, seen: true },
  { id: 5, title: 'Dune', genre: 'Sci-Fi', year: 2021, seen: false },
  { id: 6, title: 'Everything Everywhere All at Once', genre: 'Action', year: 2022, seen: true },
])

const seenShows = computed(() =>
  shows.value.filter((m) => m.seen)
)

watch(search, (newVal) => {
  console.log('Recherche :', newVal)
})

onMounted(() => {
  console.log('Application prête')
})

const onToggleSeen = (show: Show) => {
  show.seen = !show.seen
}
</script>