diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ff19b62559832e0afe72cb8f0f289d90c0ebcc5..0d3259356b4c86b90d142af0dc6f88e21e6006b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 CHANGELOG
 =========
 
+6.2.1 (29/03/2024)
+------------------
+
+- Nouveau composant UTableAjax permettant de créer un tableau avec chargement des données en Ajax et affichage avec Vue, avec son côté serveur.
+- Suppression de la fonction de formatage de date
+
 6.2.0 (26/03/2024)
 ------------------
 
diff --git a/components/UTableAjax.vue b/components/UTableAjax.vue
new file mode 100644
index 0000000000000000000000000000000000000000..75ba19f581e61adceaa3a146b932894cea0faec8
--- /dev/null
+++ b/components/UTableAjax.vue
@@ -0,0 +1,199 @@
+<template>
+    <div class="dt-bootstrap5">
+        <b-row>
+            <b-col>
+                Afficher <label>
+                <select v-model="dSize" class="form-select form-select-sm">
+                    <option v-for="ps in pageSizes" :key="ps" :value="ps">{{ ps }}</option>
+                </select>
+            </label> éléments
+            </b-col>
+            <b-col>
+                <div class="float-end">
+                    Rechercher : <label><input v-model="dSearch"
+                                               class="form-control form-inline form-control-sm"/></label>
+                </div>
+            </b-col>
+        </b-row>
+        <table class="table table-bordered dataTable mb-2" ref="tableRef">
+            <slot></slot>
+        </table>
+        <b-row>
+            <b-col>Affichage de l'élément {{ elStart }} à {{ elEnd }} sur {{ dCount }} éléments</b-col>
+            <b-col>
+                <div class="dataTables_paginate paging_simple_numbers">
+                    <b-pagination align="end" :page="page" v-model="page" :total-rows="dCount" :per-page="cSize"
+                                  last-number="true"
+                                  firstNumber="true"
+                                  prev-text="Précédent"
+                                  next-text="Suivant"
+                    />
+                </div>
+            </b-col>
+        </b-row>
+    </div>
+</template>
+<script>
+
+export default {
+    name: "UTableAjax",
+    props: {
+        id: {required: false, type: String},
+        size: {required: false, default: 10},
+        count: {required: false},
+        search: {required: false},
+        dataUrl: {required: true, type: String},
+    },
+    data()
+    {
+        return {
+            page: 1,
+            pageSizes: [10, 25, 50, 100, 'Tous'],
+            defaultSize: 10,
+            dSize: this.size,
+            dCount: this.count,
+            dSearch: this.search,
+            searchTimer: null,
+            columns: {},
+            loading: false,
+            orderCol: undefined,
+            orderDir: 'asc',
+        };
+    },
+    computed: {
+        cSize()
+        {
+            if (isNaN(this.dSize)) {
+                return 9999999999999;
+            }
+            return this.dSize;
+        },
+        storageIdentifier()
+        {
+            return 'UTableAjax-' + this.id + '-' + window.location.href;
+        },
+        elStart()
+        {
+            if (isNaN(this.dSize)) {
+                return 1;
+            }
+            return ((this.page - 1) * this.dSize) + 1;
+        },
+        elEnd()
+        {
+            if (isNaN(this.dCount)) {
+                return (this.page - 1) * this.dSize + this.dSize;
+            }
+            if (isNaN(this.dSize)) {
+                return this.dCount;
+            }
+            return Math.min(this.dCount, (this.page - 1) * this.dSize + this.dSize);
+        },
+    },
+    watch: {
+        dSize(newSize)
+        {
+            localStorage.setItem(this.storageIdentifier, newSize);
+            this.getData();
+        },
+        dSearch(newSearch)
+        {
+            const that = this;
+            clearTimeout(this.searchTimer);
+            this.searchTimer = setTimeout(() => {
+                if (this.page > 1) {
+                    this.page = 1;
+                }else {
+                    that.getData();
+                }
+            }, 500);
+        },
+        page(newPage)
+        {
+            this.getData();
+        },
+    },
+    methods: {
+        getData()
+        {
+            unicaenVue.axios.post(this.dataUrl, {
+                page: this.page,
+                size: this.dSize,
+                elStart: this.elStart,
+                elEnd: this.elEnd,
+                search: this.dSearch,
+                orderCol: this.orderCol,
+                orderDir: this.orderDir,
+            }).then(response => {
+                let data = response.data;
+                this.dCount = data.count;
+                this.$emit('data', data.data);
+            });
+        },
+        orderBy(column)
+        {
+            const element = this.columns[column];
+            for(let col in this.columns){
+                if (col != column){
+                    if (this.columns[col].classList.contains('sorting_asc')){
+                        this.columns[col].classList.remove('sorting_asc');
+                    }
+                    if (this.columns[col].classList.contains('sorting_desc')){
+                        this.columns[col].classList.remove('sorting_desc');
+                    }
+                    console.log(col)
+                }
+            }
+            let newOrder = 'asc';
+
+            if (element.classList.contains('sorting_asc')){
+                newOrder = 'desc';
+                element.classList.remove('sorting_asc');
+                element.classList.add('sorting_desc');
+            }else if (element.classList.contains('sorting_desc')){
+                element.classList.remove('sorting_desc');
+                element.classList.add('sorting_asc');
+            }else{
+                element.classList.add('sorting_asc');
+            }
+
+            this.orderCol = column;
+            this.orderDir = newOrder;
+
+            this.getData();
+        },
+    },
+    mounted()
+    {
+        this.dSize = parseInt(localStorage.getItem(this.storageIdentifier)) || this.defaultSize;
+        this.page = 1;
+        this.dSize = this.size;
+        this.dCount = this.count;
+        const headEl = this.$refs.tableRef;
+        const that = this;
+        if (headEl) {
+            // Récupération des éléments <th> qui ont l'attribut 'column'
+            const thElements = headEl.querySelectorAll('th[column]');
+            thElements.forEach(th => {
+                Array.from(th.attributes).forEach(attr => {
+                    if (attr.name == 'column') {
+                        this.columns[attr.value] = th;
+                        th.dataset.column = attr.value;
+                        th.onclick = function () {
+                            that.orderBy(this.dataset.column);
+                        }
+                        th.removeAttribute(attr.name);
+                        th.classList.add('sorting');
+                    }
+                });
+            });
+        }
+
+        this.getData();
+    },
+}
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/doc/composants.md b/doc/composants.md
new file mode 100644
index 0000000000000000000000000000000000000000..a3c7083a19bd673b48f9e847993491b36a663b0d
--- /dev/null
+++ b/doc/composants.md
@@ -0,0 +1,4 @@
+# Composants proposés en standard
+
+- [UCalendar]() Calendrier pour la saisie d'informations par mois (à documenter)
+- [UTableAjax](composants/UTableAjax.md) Permet d'afficher des tableaux de données chargés en Ajax et formatés avec Vue.js
diff --git a/doc/composants/UTableAjax.md b/doc/composants/UTableAjax.md
new file mode 100644
index 0000000000000000000000000000000000000000..6babbc2b9615076899b1d77cc32445634635deb8
--- /dev/null
+++ b/doc/composants/UTableAjax.md
@@ -0,0 +1,103 @@
+# UTableAjax
+
+Permet de dessiner un pseudo DataTable avec un chargement des données en Ajax et un affichage en VueJS.
+
+Exemple d'utilisation côté client, dans un composant Vue :
+
+```vue
+<template>
+    <u-table-ajax :data-url="this.dataUrl" @data="maj">
+        <thead>
+        <tr>
+            <th column="ID">Id</th><!-- l'attribut column doit être renseigné pour pouvoir la rendre triable -->
+            <th column="LIBELLE">Libellé</th>
+            <th column="FORMULE">Formule</th>
+            <th column="ANNEE">Année</th>
+            <th>&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+        <!-- On liste toute les lignes et on les affiche ici -->
+        <tr v-for="(line,i) in lines" :key="i">
+            <td>{{ line['ID'] }}</td>
+            <td>{{ line['LIBELLE'] }}</td>
+            <td>{{ line['FORMULE'] }}</td>
+            <td>{{ line['ANNEE'] }}</td>
+            <td><a :href="editUrl(line['ID'])">Modifier</a></td>
+        </tr>
+        </tbody>
+    </u-table-ajax>
+</template>
+
+<script>
+
+export default {
+    name: 'Test',
+    data()
+    {
+        return {
+            dataUrl: unicaenVue.url('.../data'),
+            
+            lines: [],
+        };
+    },
+    methods: {
+        maj(lines)
+        {
+            this.lines = lines;
+        },
+        editUrl(id)
+        {
+            return unicaenVue.url('mon-url/:id', {id: id});
+        },
+    },
+}
+
+</script>
+<style scoped>
+
+</style>
+```
+
+Exemple de génération de données côté serveur, dans un contrôleur :
+```php
+<?php
+
+namespace MonModule\Controller;
+
+use Laminas\Mvc\Controller\AbstractActionController;
+use UnicaenVue\Util;
+
+class TestController extends AbstractActionController
+{
+
+    public function dataAction()
+    {
+        // Requête SQL
+        // Elle doit avoir un paramètre :search pour traiter les recherches
+        // Elle ne doit pas avoir d'OrderBY : il sera ajouté ensuite selon le besoin
+        $sql = "
+        SELECT 
+          fti.id      id, 
+          fti.libelle libelle,
+          f.libelle   formule,
+          a.libelle   annee
+        FROM 
+          formule_test_intervenant fti
+          JOIN formule f ON f.id = fti.formule_id
+          JOIN annee a ON a.id = fti.annee_id
+        WHERE
+          lower(fti.libelle || ' ' || f.libelle || ' ' || a.libelle) like :search
+        ";
+
+        // renvoie un axiosModel avec les données issues du requêtage
+        $em = /* Récupération de l'entityManager de Doctrine */;
+        
+        return Util::tableAjaxData($em, $this->axios()->fromPOst(), $sql);
+    }
+}
+```
+
+Résultat :
+
+![UTableAjax](UTableAjax.png)
\ No newline at end of file
diff --git a/doc/doc.md b/doc/doc.md
index 6bfdcc3d85c1b2304a41afa8ab8b0ca0e1a2419a..6572a9e71d9a8001681bcf76eae20f155cd8641f 100644
--- a/doc/doc.md
+++ b/doc/doc.md
@@ -8,6 +8,8 @@
 
 [Partie serveur PHP](serveur.md)
 
+[Composants proposés en standard](composants.md)
+
 Pour faire vos premiers pas avec Vue :
 
 - [Doc d'introduction à VueJS de Stéphane](https://git.unicaen.fr/bouvry/presentation-dev/-/blob/master/src/vuejs.md)
diff --git a/src/Controller/Plugin/AxiosPlugin.php b/src/Controller/Plugin/AxiosPlugin.php
index 695ecc907e95e51c19a7a84e69deee48f7f50400..2179d4bd02033465e095393dd2bc2b184e7d9bd5 100755
--- a/src/Controller/Plugin/AxiosPlugin.php
+++ b/src/Controller/Plugin/AxiosPlugin.php
@@ -2,13 +2,7 @@
 
 namespace UnicaenVue\Controller\Plugin;
 
-use Doctrine\Common\Collections\Collection;
-use Doctrine\ORM\Query;
 use Laminas\Mvc\Controller\Plugin\AbstractPlugin;
-use Laminas\Mvc\Plugin\FlashMessenger\FlashMessenger;
-use Laminas\View\Model\JsonModel;
-use UnicaenVue\Axios\AxiosExtractor;
-use UnicaenVue\Axios\AxiosExtractorInterface;
 
 /**
  * Description of Axios
diff --git a/src/Util.php b/src/Util.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b64f494824a2c93dbe761f94434719c1cc89789
--- /dev/null
+++ b/src/Util.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace UnicaenVue;
+
+use Doctrine\ORM\EntityManager;
+use UnicaenVue\View\Model\AxiosModel;
+
+class Util
+{
+    static public function tableAjaxData(EntityManager $em, array $post, string $sql): AxiosModel
+    {
+        $search = $post['search'] ?? '';
+        $elStart = ($post['elStart'] ?? 1) - 1;
+        $size = $post['size'] ?? 10;
+        $orderCol = $post['orderCol'] ?? null;
+        $orderDir = ($post['orderDir'] ?? 'asc') == 'asc' ? 'asc' : 'desc';
+
+        if ($orderCol && (str_contains($orderCol, '"') || str_contains($orderCol, "'"))){
+            $orderCol = null; // protection contre les injections
+        }
+
+        $limitSql = "OFFSET :elStart ROWS FETCH FIRST :size ROWS ONLY";
+        if ($orderCol) {
+            $orderSql = "ORDER BY $orderCol $orderDir";
+        } else {
+            $orderSql = '';
+        }
+        $params = [
+            'elStart' => $elStart,
+            'size'    => $size,
+            'search'  => strtolower('%' . $search . '%'),
+        ];
+        $count = (int)$em->getConnection()->fetchOne('SELECT count(*) cc FROM (' . $sql . ') t', $params);
+        $data = $em->getConnection()->fetchAllAssociative($sql . "\n" . $orderSql . "\n" . $limitSql, $params);
+
+        $result = [
+            'count' => $count,
+            'data'  => $data,
+        ];
+
+        return new AxiosModel($result);
+    }
+}
\ No newline at end of file
diff --git a/src/View/Model/AxiosModel.php b/src/View/Model/AxiosModel.php
index a259898df830dd6c1d3f5dd8f46c89522484498b..8b1756ffde587413383e198cfe87f16fe2ff055a 100644
--- a/src/View/Model/AxiosModel.php
+++ b/src/View/Model/AxiosModel.php
@@ -2,7 +2,6 @@
 
 namespace UnicaenVue\View\Model;
 
-use Exception;
 use Laminas\View\Model\ModelInterface;
 use Traversable;