Pagination with editable table Ready
This template shows how to build a more complex pagination with editable table, values stored in URL hash and state keeping on reload
Hint: Press F
on your keyboard to view both templates and components in fullscreen and ESC
to exit the fullscreen mode. You can also open the template in a new browser window.
<!DOCTYPE html>
<html class="duet-bg-gradient duet-sticky-footer" lang="fi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>LähiTapiola</title>
<link rel="stylesheet" href="https://cdn.duetds.com/api/fonts/3.0.51/lib/localtapiola.css" integrity="sha384-5JYmtSD7nykpUvSmTW1CHMoBDkBZUpUmG0vuh+NUVtZag3F75Kr7+/JU3J7JV6Wq" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.duetds.com/api/css/4.0.45/lib/duet.min.css" integrity="sha384-UoMJnpXiN8f7fKVnTzfKfyi7LzQlApQ+WTS9O3PXlYr6CO9yzou4glfsHV747f3v" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.duetds.com/api/tokens/4.0.59/lib/tokens.custom-properties.css" integrity="sha384-AexjbYNj18dJLZR54wNVU44b/akdDc+tpbLIWhAnzjMAbwFSli2DHpSP+7NCK+Xw" crossorigin="anonymous" />
<script type="module" src="https://cdn.duetds.com/api/components/8.7.1/lib/duet/duet.esm.js" integrity="sha384-c3hLghWHTPntUxndMlK4myD7d59stA2U0EvBp2rPb3ibtacAwh56geBpq6z7nXLn" crossorigin="anonymous"></script>
<script nomodule src="https://cdn.duetds.com/api/components/8.7.1/lib/duet/duet.js" integrity="sha384-H7RH4Ssj/LmElXa1VCHFSIjvtCSoLXMiPXG/KQZTS9EvYujTo7tWaj0V6/GGkY16" crossorigin="anonymous"></script>
</head>
<body>
<style>
.duet-table-action-row.content-editable td:not(:last-child) {
border: 1px solid red;
}
.example-menu {
text-align: right;
}
.example-menu ul {
padding: 0 1rem;
}
.example-menu li {
display: inline-block;
list-style: none;
border-left: 1px solid #ccc;
padding: 0 1rem;
}
.example-menu li.menu-select {
min-width: 100px;
text-align: right;
}
.pagination-menu {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(300px, 600px) minmax(100px, 300px) minmax(100px, 150px);
grid-template-rows: 1fr;
gap: 0 0;
grid-template-areas: "pager ranger select";
justify-content: start;
align-content: center;
justify-items: baseline;
align-items: center;
}
.pagination-menu duet-pagination {
grid-area: pager;
justify-self: start;
align-self: center;
width: 100%;
}
.pagination-menu duet-range-stepper {
justify-self: end;
align-self: center;
grid-area: ranger;
}
.pagination-menu duet-select {
justify-self: end;
align-self: center;
grid-area: select;
}
@media only screen and (max-width: 48em) {
.pagination-menu {
display: block;
}
duet-range-stepper {
display: none;
}
duet-select {
display: none !important;
}
}
</style>
<duet-layout center>
<div slot="main">
<duet-card>
<duet-heading level="h1" visual-level="h3">Duet-editable-table with Duet-pagination</duet-heading>
<duet-editable-table margin="auto" sticky sticky-distance="none" variation="striped" sortable>
<div class="example-menu" slot="thead-first">
<div>
<ul role="menubar">
<li role="menuitem">
<duet-button
accessible-label="Download the table as a CSV file"
fixed
icon="action-download"
icon-size="medium"
margin="none"
padding="none"
variation="plain"
>
Download as CSV
</duet-button>
</li>
<li role="menuitem">
<duet-button
accessible-label="Copy the data to you Clipboard as csv"
fixed
icon="form-file"
icon-size="medium"
margin="none"
padding="none"
variation="plain"
>
copy
</duet-button>
</li>
<li role="menuitem" class="menu-select">
<duet-select
label-hidden
label="which types to show in table"
value="all"
variation="tiny"
id="invoice-type"
></duet-select>
</li>
</ul>
</div>
</div>
<div class="pagination-menu" slot="tfoot">
<duet-pagination take="5" visible-items="5" total="1000"></duet-pagination>
<duet-select label-hidden label="select range" value="5" variation="tiny" id="pagination-take"></duet-select>
</div>
</duet-editable-table>
<duet-paragraph> </duet-paragraph>
</duet-card>
</div>
</duet-layout>
<script>
const dayInMS = 24 * 60 * 60 * 1000
const today = new Date()
const localeDate = daysFromToday => (new Date(today.getTime() + (dayInMS * daysFromToday))).toLocaleDateString("fi")
const open = [
{
number: "1-first",
dueDate: localeDate(-20),
amount: "63,92\xa0€",
paid: false,
paymentDate: localeDate(-10)
},
]
for (i = 2; i < 22; i++) {
open.push({
number: i,
dueDate: localeDate(i * 2),
paid: false,
amount: (30 + Math.round(Math.random() * 100)) + ",00\xa0€",
paymentDate: ""
})
}
const paid = []
for (i = 1; i < 102; i++) {
paid.push({
number: 21 + i,
dueDate: localeDate(i * -11),
paid: true,
amount: ((30 + (Math.random() * 100)).toFixed(2) + "\xa0€").replace(".", ",")
})
}
const allItems = [...open, ...paid]
console.log("Item Counts", "all", allItems.length, "open", open.length, "paid", paid.length);
</script>
<script>
// Save a reference to the elements
const eTable = document.querySelector("duet-editable-table");
// defined actions - if this is not defined, the table will not have any actions
eTable.actions = [
{
"icon": "action-edit-2",
"color": "primary",
"name": "edit",
"size": "x-small",
"background": "gray-lightest",
"label": {
"fi": "Muokkaa luokkaa",
"en": "Edit category",
"sv": "Redigera kategori",
},
},
{
"icon": "action-delete",
"color": "danger",
"name": "delete",
"size": "x-small",
"background": "gray-lightest",
"label": {
"fi": "Poista",
"en": "Delete",
"sv": "Radera",
},
},
];
//defined columns, if this is set, rows must also be defined for this to have an affect
eTable.columns = [
{
sort_order: 1, direction: 1, index: 0, key: "number",
label: {
"en": "#",
"fi": "#",
"sv": "#",
},
},
{
sort_order: 2, direction: -1, index: 1, key: "dueDate",
label: {
"fi": "Due date",
"en": "Due date",
"sv": "Due date",
},
},
{
direction: 1, index: 2, key: "amount",
label: {
"fi": "Amount",
"en": "Amount",
"sv": "Amount",
},
},
{
direction: 1, index: 3, key: "paymentDate",
label: {
"fi": "Paid date",
"en": "Paid date",
"sv": "Paid date",
},
},
];
eTable.addEventListener("duetActionEvent", function(e) {
//quick demonstration of how to delete items from the rows array
console.log("Event received from duet-table: ", e.detail);
if (e.detail.action === "delete") {
eTable.rows = eTable.rows.map(item => item.meta.id !== e.detail.meta.id ? item : null).filter(item => item);
}
//quick demonstration of how to create the beginnings of a table with edit capabilities
if (e.detail.action === "edit") {
const currentRow = eTable.querySelectorAll("tbody tr")[e.detail.meta.index];
currentRow.contentEditable = true;
currentRow.classList.toggle("content-editable");
}
});
// function that sorts an array (in this case table.rows) by columns in descending order
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
let dir = 1;
if (o[0] === "-") {
dir = -1;
o = o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
//change the sort order of a specific column
function setSortOrder({ order, direction, index, column }) {
const newArray = eTable.columns.map((item) => {
if (item.key === column) {
// set sort_order to 0, which is ignored in the component as undefined
item.sort_order = 0;
// flip direction asc->desc and desc->asc
item.direction = item.direction === -1 ? 1 : -1;
}
return item;
})
// sort the array by sort_order thereby getting a 0,1,3,4,x sequence
.sort(fieldSorter(["sort_order"]))
// reset that sequence to a 1,2,3,4 situation and ignore anything where sort order was not defined
.map((item, i) => {
if (item.sort_order || item.sort_order === 0) {
item.sort_order = i + 1;
}
return item;
});
eTable.columns = newArray;
}
// Listen for toggle events
eTable.addEventListener("duetTableToggle", function(e) {
console.log("Event received from duet-table: ", e.detail);
setSortOrder({
order: e.detail.sort_order,
direction: e.detail.direction,
index: e.detail.index,
column: e.detail.key,
});
});
</script>
<script>
// Save a reference to the above pagination components
const selectTake = document.getElementById("pagination-take")
const selectInvoice = document.getElementById("invoice-type")
const pagination = document.querySelector("duet-pagination")
let urlTake = getHash(location.hash, "take") || "5"
let urlType = getHash(location.hash, "type") || "all"
let currPage = getHash(location.hash, "page") || "1"
// Set select menu items and their values
selectTake.items = [
{ label: "5", value: "5"},
{ label: "10", value: "10" },
{ label: "15", value: "15" }
]
// Set select menu items and their values
selectInvoice.items = [
{ label: "all", value: "all"},
{ label: "paid", value: "paid" },
{ label: "open", value: "open" }
]
// try to get current page from url
pagination.setAttribute("current", currPage)
// Listen for change events in the select
selectTake.addEventListener("duetChange", function (e) {
console.log("Change event detected in select:", e.detail)
pagination.setAttribute("take",e.detail.value)
location.hash = setHash(location.hash,"take", e.detail.value)
getTypeItem();
})
// Listen for change events in the select
selectInvoice.addEventListener("duetChange", function (e) {
console.log("Change event detected in select (invoice type):", e.detail)
location.hash = setHash(location.hash,"type", e.detail.value)
getTypeItem();
})
// Listen for change events in the pager
pagination.addEventListener("duetPageChange", function (e) {
console.log("Change event detected in pagination:", e.detail)
location.hash = setHash(location.hash,"page", e.detail.current)
getTypeItem();
})
//javascript functions thatuses filtering and string ops to manipulate any hashes given
function getHash(hash, key) {
return hash
.split("#")
.find((h) => h.startsWith(key))
?.replace(`${key}=`, "");
}
function setHash(hash, key, value) {
let hashArray = hash.split("#").filter((h) => !h.startsWith(key));
hashArray.push(`${key}=${value}`);
return hashArray.length > 0
? hashArray.reduce((s1, s2) => `${s1}#${s2}`)
: "";
}
function deleteHash(hash, key) {
let hashArray = hash.split("#").filter((h) => !h.startsWith(key));
return hashArray.length > 0
? hashArray.reduce((s1, s2) => `${s1}#${s2}`)
: "";
}
//simple function that uses the URL HASH as a "store" for values and states
function getTypeItem(){
// try to get defined itms from url
urlTake = getHash(location.hash, "take") || "5"
urlType = getHash(location.hash, "type") || "all"
currPage = getHash(location.hash, "page") || "1"
selectTake.setAttribute("value", urlTake)
selectInvoice.setAttribute("value", urlType)
pagination.setAttribute("take", urlTake)
pagination.setAttribute("current", currPage)
const take = Number( urlTake)
const page = Number(currPage) -1 // page starts at 1
const type = urlType
let items = []
if(type === "all"){
items=allItems;
}
if(type === "paid"){
items=paid;
}
if(type === "open"){
items=open;
}
let from = page*take
let to = from + take
const maxPages = Math.ceil(items.length / take)
// if our page counter is higher than the selection that we're working with, assume you want to "jump to the end" of the collection
if(page >= maxPages) {
location.hash = setHash(location.hash,"page", maxPages)
return getTypeItem()
}
eTable.rows =items.slice(from, to)
pagination.total = items.length;
pagination.take = take;
return {
rows: items.slice(from, to),
total: items.length,
take
}
}
//Run once for initial state
getTypeItem();
</script>
</body>
</html>
Integration
To install this template’s dependencies into your project, run:
npm install @duetds/components
npm install @duetds/css
npm install @duetds/fonts
For further guidelines, please see each package’s documentation.
Tutorials
Follow these practical tutorials to learn how to build simple page layouts using Duet’s CSS Framework, Web Components and other features:
Tutorials
Building Layouts
TutorialsUsing CLI Tools
TutorialsCreating Custom Patterns
TutorialsServer Side Rendering
TutorialsSharing Prototypes
TutorialsUsage With Markdown
Troubleshooting
If you experience any issues while using a template, please head over to the Support page for more guidelines and help.