Vue.js 3 : Création d’un CRUD optimisé avec un moteur recherche à l’aide pinia.

Introduction

Dans cet article, nous allons créer une application CRUD (Create, Read, Update, Delete) avec Vue.js 3, Pinia pour la gestion d’état et Bootstrap pour un design moderne et réactif. L’objectif est de mettre en place une structure solide en intégrant des fonctionnalités clés comme la pagination et des services pour la communication avec l’API, tout en assurant une interface utilisateur fluide et agréable grâce à Bootstrap.

Pinia, gestionnaire d’état officiel de Vue.js, se distingue par sa simplicité et sa flexibilité. Il centralise la gestion des données et simplifie la manipulation des états, même dans les applications complexes. Avec son API intuitive, nous pourrons suivre et mettre à jour les données en temps réel, tout en réagissant dynamiquement aux changements.

Nous mettrons également en place des services pour centraliser les opérations CRUD, rendant ainsi la communication avec l’API plus efficace. Ces services organiseront les requêtes asynchrones et sépareront la logique métier des composants.

Enfin, un système de pagination permettra de gérer facilement de grandes listes de données, tout en maintenant une interface cohérente et élégante avec Bootstrap, sans surcharge de code personnalisé. L’objectif est de montrer comment utiliser ces outils pour créer une application CRUD robuste, évolutive et maintenable, avec une gestion des données optimisée et une interface intuitive.

Prérequis

Avant de commencer ce tutoriel sur la création d’un système de gestion des catégories avec Pinia, assurez-vous d’avoir les éléments suivants en place, en partant du projet initialisé dans le premier article en suivant ce lien :

  • Vue.js 3 : Un projet Vue.js 3 doit être déjà créé selon les instructions du premier article, avec Pinia et Bootstrap configurés.
  • Node.js version 18 ou supérieure : Utilisez Node.js version 18 ou ultérieure pour garantir la compatibilité avec les outils modernes que nous allons utiliser.
  • Familiarité avec la ligne de commande : Une connaissance de base des commandes en ligne est nécessaire pour gérer les opérations liées à Pinia et à la gestion des catégories.
  • Yarn : Si Yarn a été utilisé dans le projet initial, assurez-vous qu’il est installé et bien configuré dans l’environnement de développement.

En vérifiant ces prérequis, vous serez prêt à suivre ce tutoriel et à développer efficacement un système de gestion des catégories dans votre application Vue.js.

Use Case

Imaginez que vous développez une application de gestion de contenu où l’administrateur peut ajouter, modifier et supprimer des catégories de contenu. Chaque catégorie est définie par un nom et une description. Pour mettre en place ce système, nous avons besoin de :

  • Ajouter une nouvelle catégorie : L’administrateur pourra créer une nouvelle catégorie en fournissant un nom et une description.
  • Lister toutes les catégories : Une vue affichera toutes les catégories existantes.
  • Modifier une catégorie : L’administrateur pourra sélectionner une catégorie pour mettre à jour ses informations.
  • Supprimer une catégorie : L’administrateur pourra supprimer une catégorie, avec confirmation avant l’action.

Pour assurer une réactivité optimale et une mise à jour en temps réel sans rechargement de la page, nous utiliserons Pinia pour gérer l’état global des catégories. Pinia facilitera l’accès et la manipulation des données à travers les différentes vues de l’application.

Ce deuxième article est une continuation directe du projet initialisé dans le premier tutoriel. Il se concentre sur l’utilisation de Pinia pour une gestion d’état avancée. En suivant ces étapes, nous construirons une application Vue.js capable de gérer les catégories de manière efficace, en utilisant les principes établis dans l’article précèdent concernant la configuration notre projet.

Démarrage du Projet Vue

Accédez au Répertoire du Projet :

Ouvrons notre terminal ou notre invite de commandes et naviguons vers le répertoire où se trouve notre projet Vue.js. Utilisons la commande suivante :

cd crud-category-with-pinia

Installer les Dépendances :

Une fois dans le répertoire du projet, assurons-nous que toutes les dépendances nécessaires sont installées. Exécutons la commande suivante :

 yarn install

Cela téléchargera et installera toutes les dépendances spécifiées dans le fichier package.json de votre projet.

Démarrer le Serveur de Développement :

Après l’installation des dépendances, nous pouvons démarrer le serveur de développement pour voir notre application en action. Utilisons la commande suivante :

yarn run dev

Cette commande lancera le serveur de développement et nous fournira une URL locale (généralement http://localhost:5173/) où nous pouvons visualiser notre application Vue.js.

http://localhost:5173/

Accéder à l’Application :

Ouvrons notre navigateur et accédons à l’URL fournie pour voir notre application Vue.js en fonctionnement.

Découpage et Organisation du Projet

Cette organisation permet de maintenir le code bien structuré, facilitant ainsi la gestion et l’évolutivité de l’application.

CRUD-CATEGORY-WITH-PINIA/
├── node_modules/
├── public/
├── src/
│   ├── assets/
│   ├── components/
│   │   └── modals/
│   │       ├── AddCategoryModal.vue
│   │       ├── DetailCategoryModal.vue
│   │       └── EditCategoryModal.vue
│   ├── router/
│   │   └── index.ts
│   ├── services/
│   │   └── categoryService.ts
│   ├── shared/
│   │   ├── apiformat.ts
│   │   └── pagination.ts
│   ├── stores/
│   │   └── useCategoryStore.ts
│   ├── views/
│   │   └── settings/
│   │       ├── CategoryList.vue
│   │       ├── CategoryIndex.vue
│   │       ├── CategoryTable.vue
│   │       └── NotFound.vue
│   ├── App.vue
│   ├── main.ts
│   ├── shims-vue.d.ts   # Fichier de déclaration pour TypeScript
    ├── types/           # Dossier pour les types personnalisés 
│   └── env.d.ts
├── .env
├── .eslintrc.cjs
├── package.json 
├── .gitignore
├── tsconfig.json 
└── vite.config.ts 
  • assets/ : Répertoire pour les fichiers statiques comme les images et les styles.
  • components/ : Composants Vue réutilisables à travers l’application.
    • modals/ : Composants modaux pour ajouter, modifier, et afficher les détails des catégories.
  • views/ : Composants de vue représentant les différentes pages ou sections de l’application.
    • settings/ : Composants liés à la gestion des paramètres, comme la liste des catégories.
  • store/ : Contient les fichiers de gestion d’état avec Pinia.
  • services/ : Fichiers pour les services d’API et autres utilitaires.
  • shared/ : Code partagé entre différents modules, comme la pagination et le formatage des API.
  • App.vue : Composant racine de l’application.
  • main.ts : Point d’entrée de l’application, où Pinia est installé et l’application Vue est montée.

Création du Service

Nous allons d’abord créer un service pour gérer les interactions avec notre API. Le code ci-dessus montre un exemple de service qui permet de gérer les requêtes HTTP pour différentes actions, comme l’ajout, la modification, et la suppression de données. Ce service est essentiel pour centraliser les appels API et faciliter leur gestion dans toute l’application.

//src/services/categoryService.ts
import type  { App } from "vue";
import type { AxiosResponse } from "axios";
import axios from "axios";
import VueAxios from "vue-axios";

class CategoryService {
  public static vueInstance: App;

  public static init(app: App<Element>) {
    CategoryService.vueInstance = app;
    CategoryService.vueInstance.use(VueAxios, axios);
    CategoryService.vueInstance.axios.defaults.baseURL =
      import.meta.env.VITE_APP_API_URL;
  }
  
  public static async post(
    resource: string,
    params: any
  ): Promise<AxiosResponse> {
    return CategoryService.vueInstance.axios.post(`${resource}`, params, {});
  }

  public static async update(
    resource: string,
    slug: string,
    params: any
  ): Promise<AxiosResponse> {
		return CategoryService.vueInstance.axios.put(
      `${resource}/${slug}`,
      params,
      {}
    );
  }
  public static async put(
    resource: string,
    params: any
  ): Promise<AxiosResponse> {
    return CategoryService.vueInstance.axios.put(`${resource}`, params);
  }

  public static async delete(resource: string): Promise<AxiosResponse> {
    
    return CategoryService.vueInstance.axios.delete(resource, {});
  }

  public static async getById(resource: string): Promise<AxiosResponse> {
   
    return CategoryService.vueInstance.axios.get(resource);
  }

  public static async getAll(resource: string): Promise<AxiosResponse> {
    
    return CategoryService.vueInstance.axios.get(resource);
  }
}

export default CategoryService;
VITE_APP_API_URL="https://apiphantom.applize.io/api/"

Nous avons également créé deux fichiers supplémentaires pour améliorer l’efficacité de notre projet :

Formatage des données API

Créer un fichier apiformat.ts dans le répertoire shared, ce fichier est responsable du formatage des données API, garantissant une manipulation cohérente et standardisée des données à travers l’application.

// shared/apiformat.ts
import type { Category } from "@/stores/useCategoryStore";

export const formatItem = (item: any): Category => {
  if (!item ) {
    throw new Error("Invalid item format");
  }

  return {
    id: item.id,
    name: item.name || "Untitled",
    description: item.description || "Untitled",
    created_at: item.created_at || "Untitled",
    updated_at: item.updated_at || "Untitled",
  };
};

export const formatCategoryData = (response: any): Category[] => {
  const dataArray = response.data;

  if (!Array.isArray(dataArray)) {
    throw new Error("Invalid response format");
  }

  return dataArray.map(formatItem);
};

Pagination

Créez un fichier pagination.ts dans le répertoire shared, qui servira à gérer la pagination de l’application de manière centralisée. Cela permettra de simplifier l’intégration et la maintenance de la logique de pagination.

// shared/pagination.ts
class Pagination<T> {
  itemsPerPage: number;
  currentPage: number;
  totalItems: number;
  items: T[];
  filters: Record<string, string>;
  sortField: string;
  sortOrder: "asc" | "desc";

  constructor(config?: Partial<Pagination<T>>) {
    this.itemsPerPage = config?.itemsPerPage || 15;
    this.currentPage = config?.currentPage || 1;
    this.totalItems = config?.totalItems || 0;
    this.items = config?.items || [];
    this.filters = config?.filters || {};
    this.sortField = config?.sortField || null;
    this.sortOrder = config?.sortOrder || "asc";
  }

  setItemsPerPage(count: number) {
    this.itemsPerPage = count;
  }

  setCurrentPage(page: number) {
    this.currentPage = page;
  }

  setTotalItems(total: number) {
    this.totalItems = total;
  }

  setItems(items: T[]) {
    this.items = items;
    if (this.items.length === 0 && this.currentPage > this.totalPages()) {
      this.currentPage = this.totalPages();
    }
  }

  addFilter(field: string, value: string) {
    this.filters[field] = value;
  }

  removeFilter(field: string) {
    delete this.filters[field];
  }

  setSortField(field: string) {
    if (this.sortField === field) {
      this.sortOrder = this.sortOrder === "asc" ? "desc" : "asc";
    } else {
      this.sortField = field;
      this.sortOrder = "asc";
    }
  }

  setSortOrder(order: "asc" | "desc") {
    this.sortOrder = order;
  }

  get offset() {
    return (this.currentPage - 1) * this.itemsPerPage;
  }

  totalPages(): number {
    return Math.ceil(this.totalItems / this.itemsPerPage);
  }

  prevPage() {
    if (this.currentPage > 1) this.currentPage--;
  }

  nextPage() {
    if (this.currentPage < this.totalPages()) this.currentPage++;
  }

  goToPage(page: number) {
    if (page >= 1 && page <= this.totalPages()) {
      this.currentPage = page;
    }
  }

  getTotalItems() {
    return this.totalItems;
  }

  get queryString() {
    const filterString = Object.entries(this.filters)
      .map(([field, value]) => {
        return `filter[${field}]=${value}`;
      })
      .join("&");

    const sortString = this.sortField
      ? `&sort=${this.sortOrder === "asc" ? "" : "-"}${this.sortField}`
      : "";

    const paginationString = `page=${this.currentPage}`;

    return `${paginationString}${
      filterString ? "&" + filterString : ""
    }${sortString}`;
  }

}

export default Pagination;

Création du Store

Nous allons commencer par ouvrir le fichier main.ts et importer createPinia. Cette importation nous permet de créer une instance de Pinia, que nous allons ensuite utiliser dans notre application. En ajoutant .use(pinia) à notre application Vue, nous installons Pinia en tant que plugin, le rendant disponible pour la gestion de l’état à travers toute l’application.

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from "pinia";
import CategoryService from './services/categoryService';
 import "bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";


const app = createApp(App)
const pinia = createPinia();

CategoryService.init(app);


app.use(router)
app.use(pinia);
app.mount('#app')

Nous allons commencer par créer un dossier store dans le répertoire src, où nous ajouterons un fichier nommé useCategoryStore.ts. Le préfixe “use” est couramment utilisé dans Vue 3 pour nommer les stores Pinia et les composables, afin de les identifier plus facilement.

Dans ce fichier, nous importerons d’abord la fonction defineStore de Pinia, qui permet de créer un store. Ensuite, nous allons définir les propriétés réactives (comme les données) et les méthodes (les actions que l’on peut effectuer sur ces données) dont nous avons besoin pour gérer les catégories. Ces propriétés et méthodes seront renvoyées sous forme d’un objet, que nous pourrons ensuite utiliser dans toute l’application pour gérer l’état des catégories.

Ce fichier centralisera toute la gestion des catégories, ce qui nous permettra de maintenir et faire évoluer cette partie de l’application de manière plus simple et organisée.

// src/store/useCategoryStore.ts
import { defineStore } from "pinia";
import { ref } from "vue";
import CategoryService from "@/services/categoryService";
import Pagination from "@/shared/pagination";
import { formatItem, formatCategoryData } from "@/shared/apiformat";

export interface Category {
  id: string;
  name: string;
  description: string;
  created_at: any;
  updated_at: any;
}

export const useCategoryStore = defineStore("category", () => {
  const errors = ref({});
  const pagination = ref(new Pagination<Category>());
  const selectedCategory = ref(null);
  const showModal = ref(false);

  function setError(error: any) {
    errors.value = { ...error };
  }

  const addCategory = async (credentials: Category) => {
    const payload = {
      name: credentials.name,
      description: credentials.description,
    };

    try {
      const { data } = await CategoryService.post("categories", payload);
      const newCategory = formatItem(data);
      pagination.value.setItems([newCategory, ...pagination.value.items]);
      pagination.value.setTotalItems(pagination.value.totalItems + 1);
    } catch (error) {
      setError(error);
    }
  };

  const deleteCategory = async (id: string) => {
    try {
      await CategoryService.delete(`categories/${id}`);
      pagination.value.setItems(
        pagination.value.items.filter((cat) => cat.id !== id)
      );
      pagination.value.setTotalItems(pagination.value.totalItems - 1);

      if (
        pagination.value.items.length === 0 &&
        pagination.value.currentPage > 1
      ) {
        pagination.value.prevPage();
      }
    } catch (error) {
      setError(error);
    }
  };

  const getCategoryById = async (id: string) => {
    try {
      const response = await CategoryService.getById(`categories/${id}`);
      console.log(response.data.data);
      console.log(formatItem(response.data.data));
      return formatItem(response.data.data);
    } catch (error) {
      setError(error);
      throw error;
    }
  };
 
  const editCategory = async (id: string, updatedCategory: Category) => {
    try {
      const { data } = await CategoryService.update(
        `categories`,
        id,
        updatedCategory
      );
      const updatedItem = formatItem(data);
      const index = pagination.value.items.findIndex((cat) => cat.id === id);
      if (index !== -1) {
        pagination.value.items[index] = updatedItem;
      }
    } catch (error) {
      setError(error);
    }
  };

  const showCategoryDetails = async (categoryId: string) => {
    try {
      selectedCategory.value = await getCategoryById(categoryId);
      console.log("id", selectedCategory.value);
      showModal.value = true;
    } catch (error) {
      console.error(
        "Erreur lors de la récupération des détails de la catégorie:",
        error
      );
    }
  };

  const getAllCategories = async (
  params: {
    filters?: Record<string, string>;
    sortField?: string;
    sortOrder?: string;
    itemsPerPage?: number;
    currentPage?: number;
    searchField?: string;
    searchQuery?: string;
  } = {}
) => {
  try {
    Object.assign(pagination.value, params);

    const requestUrl = `categories?${pagination.value.queryString}`;
    const response = await CategoryService.getAll(requestUrl);

    pagination.value.setItems(formatCategoryData(response.data));
    pagination.value.setTotalItems(
      response.data.meta?.total || pagination.value.items.length
    );
  } catch (error) {
    setError(error);
    throw error;
  }
};

 
  

  return {
    pagination,
    getAllCategories,
    addCategory,
    deleteCategory,
    getCategoryById,
    editCategory,
    showCategoryDetails,
    selectedCategory,
  };
});

Création des Vues

Pour faciliter la lecture et la compréhension, nous allons diviser la section “Création des vues” en plusieurs sous-parties distinctes. Cette approche nous permettra de suivre chaque étape de manière claire et organisée, en nous concentrant sur un aspect spécifique à chaque étape du processus.

Création de la Vue Liste des Catégories

Nous allons créer la vue principale de notre application : la liste des catégories. Cette vue sera responsable de l’affichage de toutes les catégories existantes sous forme de table. Nous aborderons la création du composant CategoryTable.vue, l’intégration avec le store Pinia pour récupérer les données, et l’application de styles à l’aide de Bootstrap pour améliorer l’apparence de la table.

<!-- src/views/settings/CategoryTable.vue -->
<template>
    <!-- table -->

    <section class="table_outer">


        <div class="container">
            <h4 class="text-center pt-4">La liste des categories</h4>

            <div class="row justify-content-center mt-4">
                <!-- entete -->

                <div class="row">
                    <div class="col-lg-6 col-md-6 col-sm-12 mb-2">
                        <div class="row">
                            <div class="col-6">
                                <!-- Button trigger modal -->
                                <button type="button" class="btn btn-primary" data-bs-toggle="modal"
                                    data-bs-target="#staticBackdrop">
                                    <i class="bi bi-plus-lg"></i>Ajouter une catégorie
                                </button>
                            </div>
                            <div class="col-3">
                                <ThemeToggle />
                            </div>
                        </div>
                    </div>
                    <div class="col-lg-6 col-md-6 col-sm-12 mb-2 ">
                        <input type="text" v-model="searchQuery" @input="updateSearchQuery"
                            class="form-control form-control-solid  ps-13" placeholder="Recherche  catégorie" />
                    </div>

                </div>
                <!-- end entete -->
                <div class="col-12">
                    <div class="card border-0 shadow">
                        <div class="card-body">
                            <div class="table-responsive">
                                <table class="table table-hover table-line table-striped table-borderless mb-0">
                                    <thead class="table-dark">
                                        <tr>

                                            <th scope="col-3">ID</th>
                                            <th scope="col-3" @click="changeSort('name')">NOM
                                                <i :class="{
                                                    'bi bi-arrow-up': sortField === 'name' && sortOrder === 'asc',
                                                    'bi bi-arrow-down': sortField === 'name' && sortOrder === 'desc',
                                                }"></i>
                                            </th>
                                            <th scope="col-4">DESCRIPTION</th>
                                            <th scope="col-3" @click="changeSort('created_at')">CREER PAR
                                                <i :class="{
                                                    'bi bi-arrow-up': sortField === 'created_at' && sortOrder === 'asc',
                                                    'bi bi-arrow-down': sortField === 'created_at' && sortOrder === 'desc',
                                                }"></i>
                                            </th>
                                            <th scope="col-2">ACTIONS</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr v-for="(item, index) in categories" :key="item.id">
                                            <td>{{ (currentPage - 1) * itemsPerPage + index
                                                + 1 }}</td>
                                            <td>{{ item.name }}</td>
                                            <td>{{ item.description}}</td>
                                            <td>{{ formatDate(item.created_at) }}</td>
                                            <td>
                                                <button type="button" class="btn btn-success btn-sm px-2 me-2"
                                                    @click="categoryStore.showCategoryDetails(item.id)"
                                                    data-bs-toggle="modal" data-bs-target="#staticBackdropEdit">
                                                    <i class="fa-solid fa-pen-to-square"></i>
                                                </button>
                                                <button type="button" class="btn btn-primary btn-sm px-2 me-2"
                                                    data-bs-toggle="modal" data-bs-target="#staticBackdrop1"
                                                    @click="categoryStore.showCategoryDetails(item.id)">
                                                    <i class="fa-solid fa-eye"></i>
                                                </button>
                                                <button type="button" class="btn btn-danger btn-sm px-2"
                                                    @click="handleDeleteClick(item.id)">
                                                    <i class="bi bi-trash-fill"></i>
                                                </button>
                                            </td>
                                        </tr>

                                    </tbody>
                                </table>
                            </div>


                        </div>
                        <!-- Pagination -->
                        <ul v-if="totalPages > 1" class="pagination justify-content-center">
                            <li class="page-item" :class="{ disabled: currentPage === 1 }">
                                <a class="page-link" href="#" @click.prevent="emitPrevPage">Previous</a>
                            </li>
                            <li class="page-item" :class="{ active: page === currentPage }" v-for="page in totalPages"
                                :key="page">
                                <a class="page-link" href="#" @click.prevent="emitGoToPage(page)">
                                    {{ page }}
                                </a>
                            </li>
                            <li class="page-item" :class="{ disabled: currentPage === totalPages }">
                                <a class="page-link" href="#" @click.prevent="emitNextPage">Next</a>
                            </li>
                        </ul>

                    </div>

                </div>
            </div>
        </div>

        <!-- toast -->
        <div id="kt_docs_toast_stack_container"
            class="toast-container position-fixed top-0 end-0 p-3 toast-container-custom">
            <div class="toast  text-white bg-success" ref="toastRef" role="alert" aria-live="assertive"
                aria-atomic="true">
                <div class="toast-header">
                    <i class="ki-duotone ki-abstract-23 fs-2 text-success me-3"><span class="path1"></span><span
                            class="path2"></span></i>
                    <strong class="me-auto">Notification</strong>
                    <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
                </div>
                <div class="toast-body">
                    {{ toastMessage }}
                </div>
            </div>
        </div>

        <!-- confirm delete -->
        <div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel"
            aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="confirmationModalLabel">Confirmation de Suppression</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        Êtes-vous sûr de vouloir supprimer cette catégorie ?
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
                        <button type="button" class="btn btn-danger" @click="confirmDelete">Supprimer</button>
                    </div>
                </div>
            </div>
        </div>

    </section>

    <!-- end table -->

</template>

<script lang="ts">
import {
    defineComponent,
    ref,
    computed,
    getCurrentInstance,
    onMounted,
    watch,
    
} from "vue";
import { useCategoryStore, type Category } from "@/stores/useCategoryStore";
import { Toast, Modal } from 'bootstrap';
import ThemeToggle from '@/components/ThemeToggle.vue'



export default defineComponent({
    name: "CategoryTable",
    components: {
        ThemeToggle
    },
    

    setup() {
        
        const searchQuery = ref("");
        const categoryStore = useCategoryStore(); // Utilisation du store de catégorie
        
        const toastMessage = ref("");
        const toastRef = ref("");
        
        const itemsPerPage = ref(15); // Nombre d'éléments par page
        const categoryToDelete = ref<string | null>(null); // ID de la catégorie à supprimer
        
        // Instance actuelle du composant
        const instance = getCurrentInstance();
        const categories = computed(() => categoryStore.pagination.items);
        
        // Observer les changements de itemsPerPage et mettre à jour les catégories en conséquence
        watch(itemsPerPage, (newCount) => {
            categoryStore.pagination.setItemsPerPage(newCount);
            categoryStore.getAllCategories();
            
        });
        watch(searchQuery, (newQuery) => {
            if (!newQuery) {
                categoryStore.pagination.setCurrentPage(1); // Réinitialiser à la première page uniquement si la recherche est vide
            }
            categoryStore.getAllCategories();
        });
        // Lors du montage du composant, récupérer les catégories
        onMounted(async () => {
            try {
                await categoryStore.getAllCategories();
                await categoryStore.pagination.items;
            } catch (error) {
                console.error("Error fetching categories on mount:", error);
            }
        });
        
        
        // Mettre à jour la requête de recherche et appliquer le filtre
        const updateSearchQuery = (e: Event) => {
            const input = e.target as HTMLInputElement;
            const query = input.value;
            categoryStore.pagination.addFilter("name",  query);
            categoryStore.getAllCategories();
        };

        // Mettre à jour le nombre d'éléments par page
        const updateItemsPerPage = async (e: Event) => {
            const select = e.target as HTMLSelectElement;
            const newItemsPerPage = Number(select.value);
            categoryStore.pagination.setItemsPerPage(newItemsPerPage);
            await categoryStore.getAllCategories();
            console.log(categoryStore.getAllCategories() )
        };
        


        
        // Calcul des pages courantes et totales pour la pagination
        const currentPage = computed(() => categoryStore.pagination.currentPage);
        const totalPages = computed(() => categoryStore.pagination.totalPages());
        const sortField = computed(() => categoryStore.pagination.sortField);
        const sortOrder = computed(() => categoryStore.pagination.sortOrder);
 
        // Passer à la page précédente et mettre à jour les catégories
        const emitPrevPage = async () => {
            categoryStore.pagination.prevPage();
            await categoryStore.getAllCategories();
            scrollToTop(); // Remonter en haut de la page
        };

        // Passer à la page suivante et mettre à jour les catégories
        const emitNextPage = async () => {
            categoryStore.pagination.nextPage();
            await categoryStore.getAllCategories();
            scrollToTop(); // Remonter en haut de la page
        };

        // Aller à une page spécifique et mettre à jour les catégories
        const emitGoToPage = async (page: number) => {
            categoryStore.pagination.goToPage(page);
            await categoryStore.getAllCategories();
            scrollToTop(); // Remonter en haut de la page
        };


        // Méthode pour afficher le toast
        const showToast = (message: string) => {
            toastMessage.value = message;
            if (toastRef.value) {
                const toastInstance = Toast.getOrCreateInstance(toastRef.value);
                toastInstance.show();
            }
        };
        
        // Fonction pour afficher la boîte de dialogue de confirmation
        const handleDeleteClick = (id: string) => {
            categoryToDelete.value = id; // Définir la catégorie à supprimer
            const confirmationModal = new Modal(document.getElementById('confirmationModal') as HTMLElement);
            confirmationModal.show(); // Afficher la boîte de dialogue
        };
        // Fonction pour confirmer la suppression
        const confirmDelete = async () => {
            if (categoryToDelete.value) {
                try {
                    await categoryStore.deleteCategory(categoryToDelete.value);
                    showToast("Category supprimée avec succès");
                    await categoryStore.getAllCategories();
                } catch (error) {
                    console.error("Erreur lors de la suppression de la catégorie : ", error);
                    showToast("Erreur lors de la suppression de la catégorie");
                } finally {
                    categoryToDelete.value = null; // Réinitialiser la catégorie à supprimer
                    const confirmationModal = Modal.getInstance(document.getElementById('confirmationModal') as HTMLElement);
                    confirmationModal?.hide(); // Cacher la boîte de dialogue
                }
            }
        };


        const handleEditClick = (id: string) => {
            instance?.emit("editCategory", id);
            categoryStore.getAllCategories();
        };
       
        // Changer le tri des catégories par champ
        const changeSort = (field: string) => {
            categoryStore.pagination.setSortField(field);
            // Récupérer toutes les catégories avec les nouveaux paramètres de tri
            categoryStore.getAllCategories();
            console.log("change sort",categoryStore.getAllCategories())
            
        };
        // defilement en haut
        const scrollToTop = () => {
            window.scrollTo({
                top: 0,
                behavior: 'smooth', // Animation fluide
            });
        };
        // formatage de la date 
        const formatDate = (dateString: string) => {
            const date = new Date(dateString);
            const options: Intl.DateTimeFormatOptions = {
                day: 'numeric',
                month: 'long',
                year: 'numeric'
            };
            return date.toLocaleDateString('fr-FR', options); // Utilise 'fr-FR' pour le format français
        };

        // Retour des éléments et fonctions nécessaires au template
        return {
            handleDeleteClick,
            handleEditClick,
            searchQuery,
            emitPrevPage,
            emitNextPage,
            emitGoToPage,
            categoryStore,
            toastMessage,
            toastRef,
            showToast,
            updateSearchQuery,
            updateItemsPerPage,
            itemsPerPage,
            changeSort,
            sortField,
            sortOrder,
            currentPage,
            totalPages,
            categories,
            scrollToTop,
            formatDate,
            confirmDelete,

        };
    },
});
</script>
<style>
@import  '../assets/main.css';


.table_outer {
    padding: 20px 0px;
    position: relative;
    min-height: 100vh;
    
}

table {
    position: sticky;
    top: 0;
    z-index: 1;
    
}

table td,
table th {
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
}

.card {
    border-radius: .5rem;
}
.container{
    width: 1300px !important;
    justify-content: center;
}
.toast-container-custom {
    z-index: 1050;
}
.toast{
background-color: red;
}
</style>


Création des Modales

Les modales jouent un rôle essentiel dans l’interaction utilisateur, en permettant l’ajout, la modification, et la visualisation des détails des catégories. Ces composants modales offrent une interface intuitive pour manipuler les catégories sans quitter la vue principale.

Dans cette sous-partie, nous allons créer plusieurs composants modales, notamment AddCategoryModal.vue, EditCategoryModal.vue, et DetailsCategoryModal.vue. Ces composants seront regroupés dans un dossier nommé modals que nous allons créer dans le répertoire components de notre projet.

Nous expliquerons en détail comment ces modales interagissent avec le store Pinia pour gérer efficacement les actions sur les catégories, comme l’ajout de nouvelles entrées, la mise à jour des informations existantes, et la consultation des détails d’une catégorie. De plus, nous verrons comment utiliser Bootstrap pour styliser ces modales, afin d’offrir une expérience utilisateur fluide, cohérente, et agréable.

En procédant de cette manière, nous pourrons facilement gérer les catégories de contenu dans notre application tout en offrant aux utilisateurs une interface claire et fonctionnelle.

  <!-- src/components/modals/AddCategoryModal.vue -->


<template>
    <!-- Modal -->
    <div class="modal fade" ref="addCategoryModalRef" id="staticBackdrop" data-bs-backdrop="static"
        data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h1 class="modal-title  text-center fs-5" id="staticBackdropLabel">Ajouter une catégorie</h1>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body" ref="formRef">
                    <div class="mb-3">
                        <label for="exampleFormControlInput1" class="form-label">Nom catégorie</label>
                        <input type="text" class="form-control" id="exampleFormControlInput1" v-model="formData.name"
                            placeholder="nom de la catégorie">
                    </div>
                    <div class="mb-3">
                        <label for="exampleFormControlTextarea1" class="form-label">Description</label>
                        <textarea class="form-control" id="exampleFormControlTextarea1" v-model="formData.description"
                            rows="3"></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-danger" data-bs-dismiss="modal">Fermer</button>
                    <button type="button" class="btn btn-success" :disabled="!categoryName.trim()"
                        @click="submit">Soumettre</button>
                </div>
            </div>
        </div>
    </div>
    <!-- toast -->
    <div id="kt_docs_toast_stack_container"
        class="toast-container position-fixed top-0 end-0 p-3 toast-container-custom">
        <div class="toast bg-success text-white" ref="toastRef" role="alert" aria-live="assertive" aria-atomic="true">
            <div class="toast-header">
                <i class="ki-duotone ki-abstract-23 fs-2 text-success me-3"><span class="path1"></span><span
                        class="path2"></span></i>
                <strong class="me-auto">Notification</strong>
                <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
            </div>
            <div class="toast-body">
                {{ toastMessage }}
            </div>

        </div>
    </div>
</template>


<script lang="ts">
import { computed, defineComponent, ref } from "vue";

import { useCategoryStore, type Category } from "@/stores/useCategoryStore";
import { Modal, Toast } from "bootstrap";




export default defineComponent({
    name: "AddCategoryModal",

    setup(_, { emit }) {
        const storeCategory = useCategoryStore();
        const toastMessage = ref<string>("");
        const toastRef = ref<null | HTMLElement>(null);
        const loading = ref<boolean>(false);
        const formRef = ref<null | HTMLFormElement>(null);
        const addCategoryModalRef = ref<null | HTMLElement>(null);
        const categoryName = computed(() => formData.value.name);



        const formData = ref<Category>({
            name: "",
            id: "",
            created_at: new Date(),
            updated_at: new Date(),
            description: "",

        });
        console.log("je suis formData",formData);


        const hideModal = (modalRef: HTMLElement | null) => {
            if (modalRef) {
                const modalInstance = Modal.getInstance(modalRef);
                if (modalInstance) {
                    modalInstance.hide();
                }
            }
        };

        const showToast = (message: string) => {
            toastMessage.value = message;
            if (toastRef.value) {
                const toastInstance = Toast.getOrCreateInstance(toastRef.value);
                toastInstance.show();
            }
        };
        const submit = async () => {
            if (!formRef.value) return;
            console.log(formRef.value);

            try {
                loading.value = true;
                await storeCategory.addCategory(formData.value);

                showToast("catégorie ajoutée avec success");
                hideModal(addCategoryModalRef.value);
                //  permet au composant courant de signaler à son environnement qu'une nouvelle catégorie a été ajoutée, offrant ainsi une manière propre et découplée pour les autres composants de réagir à cet événement.
                emit("categoryAdded");
                // Réinitialisation des champs de formData
                formData.value = {
                    name: '',
                    description: '',
                    id: '',
                    created_at: '',
                    updated_at: '',
                    
                };

            } catch (error) {
                showToast("erreur l'hors de l'ajout");
            } finally {
                loading.value = false;
            }
        };

        return {
            submit,
            formRef,
            formData,
            loading,
            addCategoryModalRef,
            toastRef,
            toastMessage,
            categoryName
            
        };
    },
});
</script>



<!-- src/components/modals/EditCategoryModal.vue -->
<template>
    <div class="modal fade" ref="editCategoryModalRef" id="staticBackdropEdit" data-bs-backdrop="static"
        data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h1 class="modal-title fs-5" id="staticBackdropLabel">Modification de la categorie</h1>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body" v-if="storeCategory.selectedCategory">
                    <div class="mb-3">
                        <label for="exampleFormControlInput1" class="form-label">Nom Categorie</label>
                        <input type="text" class="form-control" id="exampleFormControlInput1"
                            v-model="storeCategory.selectedCategory.name" placeholder="nom de la categorie">
                    </div>
                    <div class="mb-3">
                        <label for="exampleFormControlTextarea1" class="form-label">Description</label>
                        <textarea class="form-control" id="exampleFormControlTextarea1" rows="3"
                            v-model="storeCategory.selectedCategory.description"></textarea>
                    </div>

                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-danger" data-bs-dismiss="modal">Fermer</button>
                    <button type="button" class="btn btn-success" @click="submit">Soumettre</button>
                </div>
            </div>
        </div>
    </div>
    <!-- toast -->
    <div id="kt_docs_toast_stack_container"
        class="toast-container position-fixed top-0 end-0 p-3 toast-container-custom">
        <div class="toast" ref="toastRef" role="alert" aria-live="assertive" aria-atomic="true">
            <div class="toast-header">
                <i class="ki-duotone ki-abstract-23 fs-2 text-success me-3"><span class="path1"></span><span
                        class="path2"></span></i>
                <strong class="me-auto">Notification</strong>
                <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
            </div>
            <div class="toast-body">
                {{ toastMessage }}
            </div>
        </div>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useCategoryStore, type Category } from "@/stores/useCategoryStore";
import { Modal, Toast } from "bootstrap";

export default defineComponent({
    name: "EditCategoryModal",
    setup(_, { emit }) {
        const storeCategory = useCategoryStore();
        const editCategoryModalRef = ref<null | HTMLElement>(null);
        const toastMessage = ref<string>("");
        const toastRef = ref<null | HTMLElement>(null);

        const hideModal = (modalRef: HTMLElement | null) => {
            if (modalRef) {
                const modalInstance = Modal.getInstance(modalRef);
                if (modalInstance) {
                    modalInstance.hide();
                }
            }
        };
        

        const submit = async () => {
            if (!storeCategory.selectedCategory) return;
            try {
                await storeCategory.editCategory(storeCategory.selectedCategory.id, storeCategory.selectedCategory);
                if (editCategoryModalRef.value) {
                    showToast("categorie modifiee avec success");
                    hideModal(editCategoryModalRef.value);
                    emit("categoryUpdated");
                }
            } catch (error) {
                showToast("erreur l'hors de la modification");
                console.error("Error editing category:", error);
            }
        };
        const showToast = (message: string) => {
            toastMessage.value = message;
            if (toastRef.value) {
                const toastInstance = Toast.getOrCreateInstance(toastRef.value);
                toastInstance.show();
            }
        };

        return {
            storeCategory,
            submit,
            editCategoryModalRef,
             toastMessage ,
            toastRef,
            showToast,
        }
    },
})
</script> 



<!-- views/modals/DetailsCategoryModal.vue -->
<template>
    <div class="modal fade" ref="addCategoryModalRef" id="staticBackdrop1" data-bs-backdrop="static"
        data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-body" ref="formRef">
                    <div class="modal-header">
                        <h5 class="card-title text-center">Detail de la Catégorie</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="card-body" v-if="categoryStore.selectedCategory" ref="formRef">
                        <div class="mb-3">
                            <label for="exampleFormControlInput1" class="form-label"><strong>ID:</strong>{{
                                categoryStore.selectedCategory.id }} </label>
                        </div>
                        <div class="mb-3">
                            <label for="exampleFormControlInput1" class="form-label"><strong>Name:</strong> {{
                                categoryStore.selectedCategory.name }}</label>

                        </div>
                        <div class="mb-3">
                            <label for="exampleFormControlTextarea1" class="form-label"><strong>Description:</strong> {{
                                categoryStore.selectedCategory.description }}</label>

                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
import {
    defineComponent,
    ref,
    computed,

} from "vue";
import { useCategoryStore } from "@/stores/useCategoryStore";

export default defineComponent({
    name: "DetailCategory",
    
    setup() {
        
        const categoryStore = useCategoryStore(); 
        console.log("Form data edit", categoryStore.selectedCategory);
        
        // Retour des éléments et fonctions nécessaires au template
        return {
            categoryStore,
        };
    },
});
</script>

Nous avons créé un fichier CategoryList.vue dans le dossier settings sous views. Ce composant centralise la gestion des catégories, permettant d’afficher, ajouter, modifier et consulter les détails des catégories via des composants spécifiques comme TablesWidget9, AddCategory, EditCategoryModal, et DetailCategory.

<!-- views/settings/CategoryList.vue -->
<template>
    <div>
        <TablesWidget9 widget-classes="mb-5 mb-xl-8" 
            @editCategory="handleEditCategory"
             @categoryAdded="getAllCategories" 
             @categoryUpdated="getAllCategories">
            </TablesWidget9>

        <AddCategory 
        @categoryAdded="getAllCategories" />
        <DetailCategory />
        <EditCategoryModal 
        :editCategoryId="selectedCategoryId" 
        @categoryUpdated="getAllCategories" />
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
import TablesWidget9 from "@/views/CategoryTable.vue";
import { useCategoryStore } from "@/stores/useCategoryStore";
import AddCategory from "@/components/modals/AddCategoryModal.vue"
import DetailCategory from "@/components/modals/DetailCategoryModal.vue"
import EditCategoryModal from "@/components/modals/EditCategoryModal.vue";

export default defineComponent({
    name: "CategoryList",
    
    components: {
        TablesWidget9,
        AddCategory,
        DetailCategory,
        EditCategoryModal,
    },
    setup() {
        const selectedCategoryId = ref<string>("");
        const categoryStore = useCategoryStore();
        const { getAllCategories } = categoryStore;

        const handleEditCategory = (id: string) => {
            selectedCategoryId.value = id;
        };
     
        onMounted(async () => {
            try {
                await getAllCategories();
            } catch (error) {
                console.error("Error fetching categories on mount:", error);
            }
        });

        return {
            getAllCategories,
            handleEditCategory,
            selectedCategoryId,
            
        };
    },
});
</script>

Tester le CRUD

L’application est maintenant opérationnelle et accessible à l’adresse http://localhost:5173. Découvrons une illustration pour voir le résultat final en action.

Conclusion

En conclusion, ce tutoriel nous a guidé à travers la création d’un système de gestion des catégories dans une application Vue.js en utilisant Pinia pour la gestion d’état. Nous avons exploré les avantages de Pinia, ainsi que la façon de structurer et d’organiser un projet Vue.js. Avec les connaissances acquises, nous pouvons désormais étendre ce projet pour inclure d’autres fonctionnalités, ou appliquer les concepts appris à d’autres projets nécessitant une gestion d’état efficace. Continuons à explorer l’écosystème Vue.js et à expérimenter avec des fonctionnalités avancées pour enrichir nos compétences de développement front-end.

Développement frontend Nuxt.js et Vue.js * Plus de publications

Ndeye Fatou est ingénieure en cryptographie. Chaque matin, elle combine
ses connaissances en mathématiques et en informatique pour concevoir et construire des systèmes de sécurité complexes.
Elle est aussi une développeuse front-end enthousiaste et créatif avec une passion pour la création d'expériences utilisateur exceptionnelles.

0 0 votes
Évaluation de l'article
guest
0 Commentaires
Le plus ancien
Le plus récent Le plus populaire
Commentaires en ligne
Afficher tous les commentaires

Ingénierie informatique (SSII)

Applize crée des logiciels métiers pour accompagner les entreprises dans la transition vers le zéro papier.


Avez-vous un projet en tête ? Discutons-en.