Alpine Js & TailwindCSS Datatable..
Modified version of @mithicher Pen
https://codepen.io/mithicher/pen/OJyRjvb
A Pen by salmi iliass on CodePen.
Alpine Js & TailwindCSS Datatable..
Modified version of @mithicher Pen
https://codepen.io/mithicher/pen/OJyRjvb
A Pen by salmi iliass on CodePen.
<body class="antialiased sans-serif bg-gray-100"> | |
<div class="container mx-auto px-4" x-data="datatables()" x-cloak> | |
<div x-show="selectedUsers.length" class="bg-indigo-200 fixed top-4 right-4 z-40 w-1/4 shadow"> | |
<div class="container mx-auto px-4 py-4"> | |
<div class="flex md:items-center"> | |
<div class="mr-4 flex-shrink-0"> | |
<svg class="h-8 w-8 text-indigo-600" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> | |
</svg> | |
</div> | |
<div x-html="selectedUsers.length + ' rows are selected'" class="text-indigo-800 text-lg"></div> | |
</div> | |
</div> | |
</div> | |
<div class="border-b mb-4"> | |
<h1 class="text-3xl py-3 border-b mb-6">Datatable</h1> | |
<pre><strong>Source Pen :</strong> <a href="https://codepen.io/mithicher/pen/OJyRjvb" target="_blank" class="text-blue-500 hover:text-blue-700 underline">Table UI with TailwindCSS & AlpineJS by @mithicher</a> | |
<strong>Things I added :</strong> | |
Upgraded AlpineJS to v3 & TailwindCSS to v2 | |
Added Search Functionality | |
Refactor Vanilla JS Code & DOM Selectors to only AlpineJS Code.. | |
No need for specific Class names anymore | |
</pre> | |
</div> | |
<div class="mb-4 flex justify-between items-center"> | |
<div class="flex-1 pr-4"> | |
<div class="relative md:w-1/3"> | |
<input type="search" x-model="search" class="w-full pl-10 pr-4 py-2 rounded-lg shadow focus:outline-none focus:shadow-outline text-gray-600 font-medium" placeholder="Search..."> | |
<div class="absolute top-0 left-0 inline-flex items-center p-2"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-gray-400" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> | |
<rect x="0" y="0" width="24" height="24" stroke="none"></rect> | |
<circle cx="10" cy="10" r="7" /> | |
<line x1="21" y1="21" x2="15" y2="15" /> | |
</svg> | |
</div> | |
</div> | |
</div> | |
<div> | |
<div class="shadow rounded-lg flex"> | |
<div class="relative"> | |
<button @click.prevent="open = !open" class="rounded-lg inline-flex items-center bg-white hover:text-blue-500 focus:outline-none focus:shadow-outline text-gray-500 font-semibold py-2 px-2 md:px-4"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 md:hidden" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> | |
<rect x="0" y="0" width="24" height="24" stroke="none"></rect> | |
<path d="M5.5 5h13a1 1 0 0 1 0.5 1.5L14 12L14 19L10 16L10 12L5 6.5a1 1 0 0 1 0.5 -1.5" /> | |
</svg> | |
<span class="hidden md:block">Display</span> | |
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-1" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> | |
<rect x="0" y="0" width="24" height="24" stroke="none"></rect> | |
<polyline points="6 9 12 15 18 9" /> | |
</svg> | |
</button> | |
<div x-show="open" @click.away="open = false" class="z-40 absolute top-0 right-0 w-40 bg-white rounded-lg shadow-lg mt-12 -mr-1 block py-1 overflow-hidden"> | |
<template x-for="heading in headings"> | |
<label class="flex justify-start items-center text-truncate hover:bg-gray-100 px-4 py-2"> | |
<div class="text-blue-600 mr-3"> | |
<input type="checkbox" class="form-checkbox focus:outline-none focus:shadow-outline" checked @click="toggleColumn(heading.key)"> | |
</div> | |
<div class="select-none text-gray-700" x-text="heading.value"></div> | |
</label> | |
</template> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="overflow-x-auto bg-white rounded-lg shadow overflow-y-auto relative" style="height: 405px;"> | |
<table class="border-collapse table-auto w-full whitespace-no-wrap bg-white table-striped relative"> | |
<thead> | |
<tr class="text-left"> | |
<th class="py-2 px-3 sticky top-0 border-b border-indigo-200 bg-indigo-100"> | |
<label class="text-indigo-500 inline-flex justify-between items-center hover:bg-gray-300 px-2 py-2 rounded-lg cursor-pointer"> | |
<input type="checkbox" class="form-checkbox focus:outline-none focus:shadow-outline" @click="selectAllCheckbox($event);"> | |
</label> | |
</th> | |
<template x-for="heading in headings"> | |
<th class="bg-indigo-100 sticky top-0 border-b border-indigo-200 px-6 py-2 text-gray-700 font-bold tracking-wider uppercase text-xs" x-text="heading.value" x-show="columns.includes(heading.key)"></th> | |
</template> | |
</tr> | |
</thead> | |
<tbody> | |
<template x-for="user in filtered(users, 'firstName', 'lastName','emailAddress', 'phoneNumber')" :key="user.userId"> | |
<tr> | |
<td class="border-dashed border-t border-gray-300 px-3"> | |
<label class="text-blue-500 inline-flex justify-between items-center hover:bg-gray-200 px-2 py-2 rounded-lg cursor-pointer"> | |
<input type="checkbox" x-model="user.selected" class="form-checkbox rowCheckbox focus:outline-none focus:shadow-outline" :name="user.userId"> | |
</label> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('userId')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.userId"></span> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('firstName')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.firstName"></span> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('lastName')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.lastName"></span> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('emailAddress')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.emailAddress"></span> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('gender')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.gender"></span> | |
</td> | |
<td class="border-dashed border-t border-gray-300" x-show="columns.includes('phoneNumber')"> | |
<span class="text-gray-700 px-6 py-3 flex items-center" x-text="user.phoneNumber"></span> | |
</td> | |
</tr> | |
</template> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<!-- Alpine Plugins --> | |
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script> | |
<!-- Alpine Core --> | |
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script> | |
</body> | |
</html> |
function datatables() { | |
return { | |
headings: [ | |
{ | |
key: "userId", | |
value: "User ID" | |
}, | |
{ | |
key: "firstName", | |
value: "Firstname" | |
}, | |
{ | |
key: "lastName", | |
value: "Lastname" | |
}, | |
{ | |
key: "emailAddress", | |
value: "Email" | |
}, | |
{ | |
key: "gender", | |
value: "Gender" | |
}, | |
{ | |
key: "phoneNumber", | |
value: "Phone" | |
} | |
], | |
users: [ | |
{ | |
selected: false, | |
userId: 1, | |
firstName: "Cort", | |
lastName: "Tosh", | |
emailAddress: "[email protected]", | |
gender: "Male", | |
phoneNumber: "327-626-5542" | |
}, | |
{ | |
selected: false, | |
userId: 2, | |
firstName: "Brianne", | |
lastName: "Dzeniskevich", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "144-190-8956" | |
}, | |
{ | |
selected: false, | |
userId: 3, | |
firstName: "Isadore", | |
lastName: "Botler", | |
emailAddress: "[email protected]", | |
gender: "Male", | |
phoneNumber: "350-937-0792" | |
}, | |
{ | |
selected: false, | |
userId: 4, | |
firstName: "Janaya", | |
lastName: "Klosges", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "502-438-7799" | |
}, | |
{ | |
selected: false, | |
userId: 5, | |
firstName: "Freddi", | |
lastName: "Di Claudio", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "265-448-9627" | |
}, | |
{ | |
selected: false, | |
userId: 6, | |
firstName: "Oliy", | |
lastName: "Mairs", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "221-516-2295" | |
}, | |
{ | |
selected: false, | |
userId: 7, | |
firstName: "Tabb", | |
lastName: "Wiseman", | |
emailAddress: "[email protected]", | |
gender: "Male", | |
phoneNumber: "171-817-5020" | |
}, | |
{ | |
selected: false, | |
userId: 8, | |
firstName: "Joela", | |
lastName: "Betteriss", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "481-100-9345" | |
}, | |
{ | |
selected: false, | |
userId: 9, | |
firstName: "Alistair", | |
lastName: "Vasyagin", | |
emailAddress: "[email protected]", | |
gender: "Male", | |
phoneNumber: "520-669-8364" | |
}, | |
{ | |
selected: false, | |
userId: 10, | |
firstName: "Nealon", | |
lastName: "Ratray", | |
emailAddress: "[email protected]", | |
gender: "Male", | |
phoneNumber: "993-654-9793" | |
}, | |
{ | |
selected: false, | |
userId: 11, | |
firstName: "Annissa", | |
lastName: "Kissick", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "283-425-2705" | |
}, | |
{ | |
selected: false, | |
userId: 12, | |
firstName: "Nissie", | |
lastName: "Sidnell", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "754-391-3116" | |
}, | |
{ | |
selected: false, | |
userId: 13, | |
firstName: "Madalena", | |
lastName: "Fouch", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "584-300-9004" | |
}, | |
{ | |
selected: false, | |
userId: 14, | |
firstName: "Rozina", | |
lastName: "Atkins", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "792-856-0845" | |
}, | |
{ | |
selected: false, | |
userId: 15, | |
firstName: "Lorelle", | |
lastName: "Sandcroft", | |
emailAddress: "[email protected]", | |
gender: "Female", | |
phoneNumber: "882-911-7241" | |
} | |
], | |
open: false, | |
search: '', | |
columns: [], | |
get selectedUsers() { | |
return this.users.filter((user) => user.selected); | |
}, | |
init() { | |
this.columns = this.headings.map((h) => { | |
return h.key; | |
}); | |
}, | |
toggleColumn(key) { | |
this.columns.includes(key) | |
? (this.columns = this.columns.filter((i) => i !== key)) | |
: this.columns.push(key); | |
}, | |
selectAllCheckbox() { | |
let filteredUsers = this.filtered(this.users); | |
if (filteredUsers.length === this.selectedUsers.length) { | |
return filteredUsers.map((user) => (user.selected = false)); | |
} | |
filteredUsers.map((user) => (user.selected = true)); | |
}, | |
filtered(...items) { | |
// Search filter Function for any Array of Objects ! | |
// You can pass only the Array of Objects, | |
// it will search all props of every Object except "ID" | |
// Example : filtered(users) | |
// OR you can pass additional props, it will only search passed props | |
// Example : filtered(users, 'firstName', 'lastName','emailAddress', 'phoneNumber') | |
values = items.shift(); // get the list of objects | |
props = items.length ? items : null; // get list of props | |
return values.filter((i) => { | |
y = Object.assign({}, i); | |
delete y['userId']; // Specifie the id prop to remove from object | |
if (props) { | |
okeys = Object.keys(y).filter((b) => !props.includes(b)); | |
okeys.map((d) => delete y[d]); | |
} | |
itemToSearch = Object.values(y).join(); // Object to array, then join to String | |
return itemToSearch.toLowerCase().includes(this.search.toLowerCase()); // Return filtred Object | |
}); | |
} | |
}; | |
} |
[x-cloak] { | |
display: none; | |
} | |
/* input:checked + svg { | |
display: block; | |
} */ | |
[type="checkbox"] { | |
box-sizing: border-box; | |
padding: 0; | |
} | |
.form-checkbox { | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
appearance: none; | |
-webkit-print-color-adjust: exact; | |
color-adjust: exact; | |
display: inline-block; | |
vertical-align: middle; | |
background-origin: border-box; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
flex-shrink: 0; | |
color: currentColor; | |
background-color: #fff; | |
border-color: #bfc0c2; | |
border-width: 1px; | |
border-radius: 0.25rem; | |
height: 1.2em; | |
width: 1.2em; | |
} | |
.form-checkbox:checked { | |
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e"); | |
border-color: transparent; | |
background-color: currentColor; | |
background-size: 100% 100%; | |
background-position: center; | |
background-repeat: no-repeat; | |
} | |
/* width */ | |
::-webkit-scrollbar { | |
width: 0.5em; | |
} | |
/* Track */ | |
::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
/* Handle */ | |
::-webkit-scrollbar-thumb { | |
background: #7e8590; | |
} | |
/* Handle on hover */ | |
::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} |
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.4/tailwind.min.css" rel="stylesheet" /> |