Bootstrap 5
Table Dynamique
Data Table
Table Responsive
Snippets Html
Exemples de tables dynamiques avec fonctionnalités avancées : tri, recherche, pagination, en Bootstrap 5.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="copyright" content="MEZGANI Said" />
<meta name="author" content="AngularForAll" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Snippets Table Dynamic 01 | AngularForAll</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
<!-- SortableJS pour le drag & drop -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container-custom {
max-width: 1400px;
margin: 0 auto;
}
/* Card principale */
.main-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
animation: fadeInUp 0.6s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Header */
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border: none;
}
.card-header h2 {
font-weight: 700;
margin-bottom: 0.5rem;
}
/* Formulaire d'ajout */
.add-form {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 15px;
margin-bottom: 1.5rem;
}
/* Table styles */
.table-container {
overflow-x: auto;
border-radius: 15px;
}
.table {
margin-bottom: 0;
min-width: 800px;
}
.table thead th {
background: #2c3e50;
color: white;
font-weight: 600;
padding: 1rem;
border: none;
position: sticky;
top: 0;
z-index: 10;
}
.table tbody tr {
cursor: grab;
transition: all 0.3s ease;
background: white;
}
.table tbody tr:active {
cursor: grabbing;
}
.table tbody tr:hover {
background: #f8f9fa;
transform: scale(1.01);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.table tbody tr.dragging {
opacity: 0.5;
cursor: grabbing;
}
.table tbody td {
padding: 1rem;
vertical-align: middle;
border-bottom: 1px solid #eef2f6;
}
/* Handle de drag */
.drag-handle {
cursor: grab;
font-size: 1.2rem;
color: #95a5a6;
transition: all 0.3s;
}
.drag-handle:active {
cursor: grabbing;
}
.drag-handle:hover {
color: #667eea;
}
/* Badges de statut */
.badge-status {
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-weight: 500;
font-size: 0.75rem;
}
.badge-active {
background: #d4edda;
color: #155724;
}
.badge-inactive {
background: #f8d7da;
color: #721c24;
}
.badge-pending {
background: #fff3cd;
color: #856404;
}
/* Boutons d'action */
.action-btn {
padding: 0.3rem 0.6rem;
margin: 0 0.2rem;
border-radius: 8px;
transition: all 0.3s;
}
.action-btn:hover {
transform: translateY(-2px);
}
/* Animation pour les nouvelles lignes */
@keyframes highlight {
0% {
background-color: #fff3cd;
}
100% {
background-color: transparent;
}
}
.row-highlight {
animation: highlight 1.5s ease;
}
/* Modal styles */
.modal-content {
border-radius: 20px;
}
/* Statistiques */
.stats-card {
background: white;
border-radius: 15px;
padding: 1rem;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
/* Responsive */
@media (max-width: 768px) {
body {
padding: 1rem;
}
.card-header {
padding: 1.5rem;
}
.add-form {
padding: 1rem;
}
}
/* Scrollbar personnalisée */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body>
<div class="container-custom">
<div class="main-card">
<!-- Header -->
<div class="card-header">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h2>
<i class="bi bi-table"></i>
Table Dynamique avec Drag & Drop
</h2>
<p class="mb-0 opacity-75">
Glissez-déposez les lignes pour les réorganiser | Cliquez sur l'icône <i class="bi bi-grip-vertical"></i> pour déplacer
</p>
</div>
<button class="btn btn-light" onclick="exportToCSV()">
<i class="bi bi-download"></i> Exporter CSV
</button>
</div>
</div>
<div class="p-4">
<!-- Statistiques -->
<div class="row g-3 mb-4">
<div class="col-md-3 col-6">
<div class="stats-card">
<i class="bi bi-people fs-2 text-primary"></i>
<h3 class="mb-0 mt-2" id="totalCount">0</h3>
<small class="text-muted">Total Lignes</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stats-card">
<i class="bi bi-check-circle fs-2 text-success"></i>
<h3 class="mb-0 mt-2" id="activeCount">0</h3>
<small class="text-muted">Actifs</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stats-card">
<i class="bi bi-clock-history fs-2 text-warning"></i>
<h3 class="mb-0 mt-2" id="pendingCount">0</h3>
<small class="text-muted">En attente</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stats-card">
<i class="bi bi-building fs-2 text-info"></i>
<h3 class="mb-0 mt-2" id="departmentCount">0</h3>
<small class="text-muted">Départements</small>
</div>
</div>
</div>
<!-- Formulaire d'ajout -->
<div class="add-form">
<h5 class="mb-3">
<i class="bi bi-plus-circle"></i>
Ajouter une nouvelle ligne
</h5>
<div class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="inputName" placeholder="Nom complet">
</div>
<div class="col-md-2">
<input type="email" class="form-control" id="inputEmail" placeholder="Email">
</div>
<div class="col-md-2">
<select class="form-select" id="inputDepartment">
<option value="IT">IT</option>
<option value="Marketing">Marketing</option>
<option value="Ventes">Ventes</option>
<option value="RH">RH</option>
<option value="Finance">Finance</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="inputStatus">
<option value="active">Actif</option>
<option value="inactive">Inactif</option>
<option value="pending">En attente</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-primary w-100" onclick="addRow()">
<i class="bi bi-plus-lg"></i> Ajouter
</button>
</div>
</div>
</div>
<!-- Table avec Drag & Drop -->
<div class="table-container">
<table class="table table-hover" id="dataTable">
<thead>
<tr>
<th style="width: 50px">
<i class="bi bi-grip-vertical"></i>
</th>
<th>#</th>
<th>Nom</th>
<th>Email</th>
<th>Département</th>
<th>Statut</th>
<th>Date d'ajout</th>
<th style="width: 100px">Actions</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Les lignes seront ajoutées dynamiquement -->
</tbody>
</table>
</div>
<!-- Message si aucune donnée -->
<div id="emptyMessage" class="text-center py-5 text-muted" style="display: none;">
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">Aucune donnée. Cliquez sur "Ajouter" pour commencer.</p>
</div>
</div>
</div>
</div>
<!-- Modal de confirmation pour la suppression -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirmer la suppression</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
Êtes-vous sûr de vouloir supprimer cette ligne ?
</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" id="confirmDeleteBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Données initiales
let rowId = 4;
let deleteTargetId = null;
let data = [
{ id: 1, name: "Jean Dupont", email: "jean@example.com", department: "IT", status: "active", date: "2024-01-15" },
{ id: 2, name: "Marie Martin", email: "marie@example.com", department: "Marketing", status: "active", date: "2024-01-16" },
{ id: 3, name: "Pierre Durand", email: "pierre@example.com", department: "Ventes", status: "pending", date: "2024-01-17" }
];
// Initialisation
let sortable = null;
function init() {
renderTable();
initDragAndDrop();
updateStats();
}
// Rendre la table
function renderTable() {
const tbody = document.getElementById('tableBody');
const emptyMessage = document.getElementById('emptyMessage');
if (data.length === 0) {
tbody.innerHTML = '';
emptyMessage.style.display = 'block';
return;
}
emptyMessage.style.display = 'none';
tbody.innerHTML = data.map((item, index) => `
<tr data-id="${item.id}">
<td class="drag-handle-cell">
<i class="bi bi-grip-vertical drag-handle"></i>
</td>
<td>${index + 1}</td>
<td>
<strong>${escapeHtml(item.name)}</strong>
</td>
<td>${escapeHtml(item.email)}</td>
<td>
<span class="badge bg-secondary">${escapeHtml(item.department)}</span>
</td>
<td>
<span class="badge-status ${getStatusClass(item.status)}">
${getStatusText(item.status)}
</span>
</td>
<td>${formatDate(item.date)}</td>
<td>
<button class="btn btn-sm btn-outline-primary action-btn" onclick="editRow(${item.id})" title="Modifier">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger action-btn" onclick="confirmDelete(${item.id})" title="Supprimer">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`).join('');
updateRowNumbers();
}
// Mettre à jour les numéros de ligne
function updateRowNumbers() {
const rows = document.querySelectorAll('#tableBody tr');
rows.forEach((row, index) => {
row.cells[1].textContent = index + 1;
});
}
// Initialiser le Drag & Drop
function initDragAndDrop() {
const tbody = document.getElementById('tableBody');
if (sortable) {
sortable.destroy();
}
sortable = new Sortable(tbody, {
handle: '.drag-handle',
animation: 300,
ghostClass: 'dragging',
onEnd: function() {
// Mettre à jour l'ordre des données
const newOrder = [];
const rows = document.querySelectorAll('#tableBody tr');
rows.forEach(row => {
const id = parseInt(row.dataset.id);
const item = data.find(d => d.id === id);
if (item) newOrder.push(item);
});
data = newOrder;
updateRowNumbers();
showToast('Ordre mis à jour', 'success');
}
});
}
// Ajouter une ligne
function addRow() {
const name = document.getElementById('inputName').value.trim();
const email = document.getElementById('inputEmail').value.trim();
const department = document.getElementById('inputDepartment').value;
const status = document.getElementById('inputStatus').value;
if (!name || !email) {
showToast('Veuillez remplir tous les champs', 'danger');
return;
}
if (!isValidEmail(email)) {
showToast('Email invalide', 'danger');
return;
}
const newItem = {
id: ++rowId,
name: name,
email: email,
department: department,
status: status,
date: new Date().toISOString().split('T')[0]
};
data.push(newItem);
renderTable();
initDragAndDrop();
updateStats();
// Réinitialiser les champs
document.getElementById('inputName').value = '';
document.getElementById('inputEmail').value = '';
// Mettre en surbrillance la nouvelle ligne
setTimeout(() => {
const newRow = document.querySelector(`#tableBody tr[data-id="${newItem.id}"]`);
if (newRow) newRow.classList.add('row-highlight');
}, 100);
showToast('Ligne ajoutée avec succès', 'success');
}
// Modifier une ligne
function editRow(id) {
const item = data.find(d => d.id === id);
if (!item) return;
const newName = prompt('Nouveau nom:', item.name);
if (newName && newName.trim()) {
item.name = newName.trim();
}
const newEmail = prompt('Nouvel email:', item.email);
if (newEmail && isValidEmail(newEmail)) {
item.email = newEmail;
}
const newDepartment = prompt('Nouveau département (IT, Marketing, Ventes, RH, Finance):', item.department);
if (newDepartment && ['IT', 'Marketing', 'Ventes', 'RH', 'Finance'].includes(newDepartment)) {
item.department = newDepartment;
}
renderTable();
initDragAndDrop();
updateStats();
showToast('Ligne modifiée', 'success');
}
// Confirmer la suppression
function confirmDelete(id) {
deleteTargetId = id;
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
// Supprimer une ligne
function deleteRow() {
if (deleteTargetId) {
const index = data.findIndex(d => d.id === deleteTargetId);
if (index !== -1) {
data.splice(index, 1);
renderTable();
initDragAndDrop();
updateStats();
showToast('Ligne supprimée', 'success');
}
deleteTargetId = null;
}
}
// Mettre à jour les statistiques
function updateStats() {
document.getElementById('totalCount').textContent = data.length;
document.getElementById('activeCount').textContent = data.filter(d => d.status === 'active').length;
document.getElementById('pendingCount').textContent = data.filter(d => d.status === 'pending').length;
const uniqueDepts = new Set(data.map(d => d.department));
document.getElementById('departmentCount').textContent = uniqueDepts.size;
}
// Exporter en CSV
function exportToCSV() {
if (data.length === 0) {
showToast('Aucune donnée à exporter', 'warning');
return;
}
const headers = ['Nom', 'Email', 'Département', 'Statut', 'Date'];
const rows = data.map(item => [
item.name,
item.email,
item.department,
getStatusText(item.status),
item.date
]);
const csvContent = [headers, ...rows]
.map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.setAttribute('download', 'table_data.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showToast('Export CSV réussi', 'success');
}
// Utilitaires
function getStatusClass(status) {
switch(status) {
case 'active': return 'badge-active';
case 'inactive': return 'badge-inactive';
case 'pending': return 'badge-pending';
default: return 'badge-secondary';
}
}
function getStatusText(status) {
switch(status) {
case 'active': return 'Actif';
case 'inactive': return 'Inactif';
case 'pending': return 'En attente';
default: return status;
}
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR');
}
function isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function showToast(message, type = 'success') {
// Créer un toast temporaire
const toast = document.createElement('div');
toast.className = `toast-notification alert alert-${type}`;
toast.innerHTML = `
<i class="bi bi-${type === 'success' ? 'check-circle' : type === 'danger' ? 'exclamation-triangle' : 'info-circle'}"></i>
${message}
`;
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
animation: slideIn 0.3s ease;
background: ${type === 'success' ? '#28a745' : type === 'danger' ? '#dc3545' : '#ffc107'};
color: white;
padding: 1rem 1.5rem;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Initialiser
document.addEventListener('DOMContentLoaded', () => {
init();
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
deleteRow();
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
});
});
// Styles d'animation supplémentaires
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
Ouvrir cet aperçu dans un nouvel onglet du navigateur
🔗 Ouvrir dans le navigateur