Editable Table Ready
Editable Table is a functional component for enhancing tabular data with editable possibilities. It is based upon duet-table but is intended to take a Javascript Map of Records and map them into grouped sections
It’s important to understand that this component only give you the tools to represent a map in a visual way, and connect various "actions" to each line item. What the line item is and which actions you choose to run are your decisions, any action given results an event that contains information about what action was clicked/pressed and which line item it originated from. The rest of any logic will have to be provided by you.
Examples #
<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;
}
</style>
<duet-layout center>
<div slot="main">
<duet-card>
<duet-heading level="h1" visual-level="h3">Duet-editable-table simple version</duet-heading>
<duet-editable-table id="duet-editable-table_simple_table" margin="auto" sticky sticky-distance="none" variation="striped">
<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>
</ul>
</div>
</div>
</duet-editable-table>
</duet-card>
</div>
</duet-layout>
<script>
(function (){
// Save a reference to the elements
const eTable = document.querySelector("#duet-editable-table_simple_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: "name",
label: {
"fi": "Name",
"en": "Name",
"sv": "Name",
},
},
{
sort_order: 2, direction: -1, index: 1, key: "last_name",
label: {
"fi": "Last name",
"en": "Last name",
"sv": "Last name",
},
},
{
direction: 1, index: 2, key: "age",
label: {
"fi": "Age",
"en": "Age",
"sv": "Age",
},
},
{
direction: 1, index: 3, key: "hidden_age",
label: {
"fi": "hidden age",
"en": "hidden age",
"sv": "hidden age",
},
},
];
//defined rows, if this is set, columns must also be defined for this to have an affect
eTable.rows = [{
"name": "Jenn",
"last_name": "Woodburne",
"age": 65,
"hidden_age": 25,
"meta": {
"id": "1",
},
}, {
"name": "Symon",
"last_name": "Effemy",
"age": 91,
"hidden_age": 25,
"meta": {
"id": "2",
},
}, {
"name": "Shari",
"last_name": "Pattlel",
"age": 81,
"hidden_age": 25,
"meta": {
"id": "3",
},
}, {
"name": "Trudy",
"last_name": "MacRitchie",
"age": 56,
"hidden_age": 25,
"meta": {
"id": "4",
},
}, {
"name": "Dick",
"last_name": "Gorgen",
"age": 69,
"hidden_age": 25,
"meta": {
"id": "5",
},
}, {
"name": "Pia",
"last_name": "Siemens",
"age": 93,
"hidden_age": 25,
"meta": {
"id": "6",
},
}, {
"name": "Fred",
"last_name": "Baslter",
"age": 78,
"hidden_age": 25,
"meta": {
"id": "7",
},
}, {
"name": "Fred",
"last_name": "Alster",
"age": 54,
"hidden_age": 25,
"meta": {
"id": "8",
},
}, {
"name": "Mair",
"last_name": "Camier",
"age": 76,
"hidden_age": 25,
"meta": {
"id": "9",
},
}, {
"name": "Meade",
"last_name": "Hansana",
"age": 68,
"hidden_age": 25,
"meta": {
"id": "10",
},
}];
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];
console.log(currentRow);
currentRow.contentEditable = true;
currentRow.classList.toggle("content-editable");
}
});
}())
</script>
<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;
}
</style>
<duet-layout center>
<div slot="main">
<duet-card>
<duet-heading level="h1" visual-level="h3">Duet-editable-table simple version</duet-heading>
<duet-editable-table id="duet-editable-table_simple_table_with_pagination" margin="auto" sticky sticky-distance="none" variation="striped">
<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>
</ul>
</div>
</div>
<div class="pagination-advanced" slot="tfoot">
<style>
.pagination-advanced {
text-align: center;
margin: 0 auto;
text-align: center;
margin: 0 auto;
display: flex;
justify-content: space-evenly;
align-items: baseline;
}
@media only screen and (max-width: 48em) {
.pagination-advanced .pagination-menu {
display: block;
}
duet-range-stepper {
display: none;
}
}
</style>
<duet-pagination class="pagination-menu" total="500" visible-items="4"></duet-pagination>
<duet-range-stepper></duet-range-stepper>
</div>
</duet-editable-table>
</duet-card>
</div>
</duet-layout>
<script>
(function(){
// Save a reference to the elements
const eTable = document.querySelector("#duet-editable-table_simple_table_with_pagination");
// 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: "name",
label: {
"fi": "Name",
"en": "Name",
"sv": "Name",
},
},
{
sort_order: 2, direction: -1, index: 1, key: "last_name",
label: {
"fi": "Last name",
"en": "Last name",
"sv": "Last name",
},
},
{
direction: 1, index: 2, key: "age",
label: {
"fi": "Age",
"en": "Age",
"sv": "Age",
},
},
{
direction: 1, index: 3, key: "hidden_age",
label: {
"fi": "hidden age",
"en": "hidden age",
"sv": "hidden age",
},
},
];
//defined rows, if this is set, columns must also be defined for this to have an affect
eTable.rows = [{
"name": "Jenn",
"last_name": "Woodburne",
"age": 65,
"hidden_age": 25,
"meta": {
"id": "1",
},
}, {
"name": "Symon",
"last_name": "Effemy",
"age": 91,
"hidden_age": 25,
"meta": {
"id": "2",
},
}, {
"name": "Shari",
"last_name": "Pattlel",
"age": 81,
"hidden_age": 25,
"meta": {
"id": "3",
},
}, {
"name": "Trudy",
"last_name": "MacRitchie",
"age": 56,
"hidden_age": 25,
"meta": {
"id": "4",
},
}, {
"name": "Dick",
"last_name": "Gorgen",
"age": 69,
"hidden_age": 25,
"meta": {
"id": "5",
},
}, {
"name": "Pia",
"last_name": "Siemens",
"age": 93,
"hidden_age": 25,
"meta": {
"id": "6",
},
}, {
"name": "Fred",
"last_name": "Baslter",
"age": 78,
"hidden_age": 25,
"meta": {
"id": "7",
},
}, {
"name": "Fred",
"last_name": "Alster",
"age": 54,
"hidden_age": 25,
"meta": {
"id": "8",
},
}, {
"name": "Mair",
"last_name": "Camier",
"age": 76,
"hidden_age": 25,
"meta": {
"id": "9",
},
}, {
"name": "Meade",
"last_name": "Hansana",
"age": 68,
"hidden_age": 25,
"meta": {
"id": "10",
},
}];
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);
}
});
}())
</script>
<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;
}
</style>
<duet-layout center>
<div slot="main">
<duet-card>
<form action="#" novalidate>
<duet-fieldset label="Editable-table header slot">
<duet-choice-group
direction="horizontal"
label="Check to enable a column"
name="group"
responsive
value="one"
>
<duet-checkbox checked data-key="name" label="Name"></duet-checkbox>
<duet-checkbox checked data-key="last_name" label="Last name"></duet-checkbox>
<duet-checkbox checked data-key="age" label="Age"></duet-checkbox>
<duet-checkbox data-key="hidden_age" label="Hidden age"></duet-checkbox>
</duet-choice-group>
</duet-fieldset>
</form>
<form action="#" novalidate>
<duet-fieldset label="Editable-table footer slot">
<duet-button id="flipit" variation="secondary"> Flip Column Order</duet-button>
</duet-fieldset>
</form>
</duet-card>
<duet-card>
<duet-heading level="h1" visual-level="h3">Duet-editable-table with inline actions</duet-heading>
<duet-editable-table id="duet-editable-table_table_with_actions_and_sorting" margin="auto" sortable sticky sticky-distance="none" variation="striped">
<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>
</ul>
</div>
</div>
</duet-editable-table>
</duet-card>
</div>
</duet-layout>
<script>
(function(){
// Save a reference to the elements
const flipit = document.querySelector("#flipit");
const eTable = document.querySelector("#duet-editable-table_table_with_actions_and_sorting");
const form = document.querySelectorAll("duet-checkbox");
const columnSelector = document.querySelectorAll("duet-checkbox");
// Listen for change events in the select
flipit.addEventListener("click", function(e) {
eTable.columns = eTable.columns.reverse().map((item, index) => {
item.index = index;
return item;
});
});
// 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: "name",
label: {
"fi": "Name",
"en": "Name",
"sv": "Name",
},
},
{
sort_order: 2, direction: -1, index: 1, key: "last_name",
label: {
"fi": "Last name",
"en": "Last name",
"sv": "Last name",
},
},
{
direction: 1, index: 2, key: "age",
label: {
"fi": "Age",
"en": "Age",
"sv": "Age",
},
},
{
direction: 1, index: 3, key: "hidden_age", display: false,
label: {
"fi": "hidden age",
"en": "hidden age",
"sv": "hidden age",
},
},
];
//defined rows, if this is set, columns must also be defined for this to have an affect
eTable.rows = [{
"name": "Jenn",
"last_name": "Woodburne",
"age": 65,
"hidden_age": 25,
"meta": {
"id": "1",
},
}, {
"name": "Symon",
"last_name": "Effemy",
"age": 91,
"hidden_age": 25,
"meta": {
"id": "2",
},
}, {
"name": "Shari",
"last_name": "Pattlel",
"age": 81,
"hidden_age": 25,
"meta": {
"id": "3",
},
}, {
"name": "Trudy",
"last_name": "MacRitchie",
"age": 56,
"hidden_age": 25,
"meta": {
"id": "4",
},
}, {
"name": "Dick",
"last_name": "Gorgen",
"age": 69,
"hidden_age": 25,
"meta": {
"id": "5",
},
}, {
"name": "Pia",
"last_name": "Siemens",
"age": 93,
"hidden_age": 25,
"meta": {
"id": "6",
},
}, {
"name": "Fred",
"last_name": "Baslter",
"age": 78,
"hidden_age": 25,
"meta": {
"id": "7",
},
}, {
"name": "Fred",
"last_name": "Alster",
"age": 54,
"hidden_age": 25,
"meta": {
"id": "8",
},
}, {
"name": "Mair",
"last_name": "Camier",
"age": 76,
"hidden_age": 25,
"meta": {
"id": "9",
},
}, {
"name": "Meade",
"last_name": "Hansana",
"age": 68,
"hidden_age": 25,
"meta": {
"id": "10",
},
}];
// 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,
});
});
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);
}
});
form.forEach(item => {
item.addEventListener("duetChange", function(e) {
const target = e.target.getAttribute("data-key");
const state = e.detail.checked;
eTable.columns = eTable.columns.map((item) => {
if (item.key === target) {
item.display = state;
}
return item;
});
});
});
}())
</script>
Properties #
Property | Attribute | Description | Type | Default |
---|---|---|---|---|
accessibleRole | accessible-role | Exposes the aria role for optimizing accessibility. | string | undefined |
actions | -- | Define actions for all items in a table An alternative to inline HTML table. required for Sortable tables | { icon: string; color: string; background: string; size: DuetActionButtonIconSize; name: string; map?: string[]; label?: DuetLangObject; }[] | undefined |
breakpoint | breakpoint | Duet-table: By default the table is responsive - it will be flattened at narrow viewport widths. This prop controls the breakpoint at which the table should be rendered as a regular table. Set to "none" to disable the responsive functionality. Set to "none-scrollable" to disable responsive functionality and allow horizontal scrolling - this is useful for comparison tables where it's important to maintain column and row layout. | "large" | "medium" | "none" | "none-scrollable" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "xxx-small" | "small" |
columns | -- | Define columns for a table An alternative to inline HTML table. required for Sortable tables | DuetEditableTableColumn[] | undefined |
groupId | group-id | Optional id that get passed to the table and used to setup ::parts | string | "" |
hideHeadVisually | hide-head-visually | Hide a thead section visually. The content is still available to screen readers. | boolean | false |
margin | margin | Duet-table: margin of the component. | "auto" | "none" | "auto" |
rows | -- | Define rows for a table An alternative to inline HTML table. required for Sortable tables | Record<string, any>[] | undefined |
sortable | sortable | Controls whether the table is sortable by headers | boolean | false |
sticky | sticky | Duet-table: Controls whether the table has a sticky header. Sticky headers are not compatible with breakpoint="none-scrollable". | boolean | false |
stickyDistance | sticky-distance | Duet-table: Adjust the distance from top of the viewport (in pixels) when the table header becomes sticky. | "none" | "with-links" | "without-links" | "with-links" |
theme | theme | Theme of the table. | "" | "default" | "turva" | "" |
variation | variation | Duet-table: Style variation of the table. | "fixed" | "plain" | "striped" | "striped" |
Events #
Event | Description | Type |
---|---|---|
duetMenuClick | Event emitted when table is sortable and a header item is clicked/enter is pressed | CustomEvent<{ component: "duet-editable-table"; originalEvent: KeyboardEvent | MouseEvent; }> |
duetTableToggle | Event emitted when table is sortable and a header item is clicked/enter is pressed | CustomEvent<{ component: "duet-editable-table"; sort_order?: number; direction: 1 | -1; index: number; key: string; originalEvent: KeyboardEvent | MouseEvent; }> |
Usage #
This section includes guidelines for designers and developers about the usage of this component in different contexts.
The component is automatically Theme aware, and will try to add the given theme to any colors set in actions.
When to use #
- When you need to display tabular data with actions associated with them.
- When you want the user to be able to interact with each line item.
When not to use #
- To display name/value pairs such as terms and definitions. Use list component instead.
- To display a glossary list. Use list component instead.
Accessibility #
This component has been validated to meet the WCAG 2.1 AA accessibility guidelines. You can find additional information regarding accessibility of this component below.
Integration
For integration, event and theming guidelines, please see Using Components. This documentation explains how to implement and use Duet’s components across different technologies like Angular, React or Vanilla JavaScript.
Tutorials
Follow these practical tutorials to learn how to build simple page layouts using Duet’s CSS Framework, Web Components and other features:
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 component, please head over to the Support page for more guidelines and help.