This is a hierarchical table component for Svelte. This means that it is a table that may be provided with items that contain items, and said contained items need to be displayed as a sub-table. The process can be repeated, producing an arbitrary amount of nested tables. An image is worth a thousand words:
The styling shown in the example is not provided in the component itself. It must be provided by assigning a CSS
class to the table or by styling the <table>
element and its sub-elements (<th>
, <td>
, etc). Inner tables
inherit any CSS classes that are applied to the parent, but further styling can be applied down the sub tables. See
the Styling the Table section of this ReadMe for details on the matter.
- It is a recursive component.
- Sub-tables can show a path and a level on top of it (this uses the table's
<caption>
element). - Sub-tables can be targetted for further styling.
- It can group items so all items with sub-items are grouped together either before or after the items with no sub-items.
- The summary of the elements with sub-items can be specified freely using either the value of a property,
using completely arbitrary logic via a function, or creating HTML markup using the
summary
slot. - The path in sub-tables can be generated by the value of a property or by using a function.
- Column data can be rendered using the
datacell
slot to allow for complex HTML content. - If needed, the entire row can be freely designed using the
datarow
slot. - Column definitions allow the definition of a
renderValue()
function to easily control data formatting or support computed columns.
Install the package:
npm i svelte-htable
Now use it as <Htable />
in a page or component. The following example is in TypeScript to show how to extend the
Item
type to have Intellisense. This is optional, as all of TypeScript. Feel free to use JavaScript if you want.
NOTE: This is an example use in a Svelte page (it receives the data from its load()
function).
<script lang="ts">
import
Htable, { // Table component.
CaptionOrder, // Enumeration to control the order of the data in captions.
ItemGrouping, // Enumeration to control the item grouping behavior.
type Column, // TypeScript type. It represents a single table column definition.
type Item, // Base TypeScript item type. Extend this type as shown below.
} from "svelte-htable";
interface DataItem extends Item {
first_name: string;
last_name: string;
email: string;
gender: string;
country_code: string;
manager_id: number;
}
// This page expects the data to be provided through its load() function (Svelte stuff).
export let data: { data: DataItem[] };
// Define your table columns.
const columns: Column[] = [
{
key: "id", // property name in the JS object
title: "ID", // title shown in the column header
},
{
key: "first_name",
title: "First Name",
},
{
key: "last_name",
title: "Last Name",
},
{
key: "birth_date",
title: "Birth Date",
renderValue: (i, k) => new Date(i[k]).toLocaleDateString(), // Date formatting
},
{
key: "age", // Computed column.
title: "Age",
renderValue: (i, k) => {
const diff = Date.now() - Date.parse(i.birth_date);
return Math.floor(
diff / (365 * 24 * 60 * 60 * 1000)
).toString();
},
},
{
key: "gender",
title: "Gender",
},
{
key: "country_code",
title: "Country Code",
},
{
key: "manager_id",
title: "Manager's ID",
},
];
</script>
<!-- Now use the component. -->
<Htable
items={data.data}
{columns}
level={true}
showPath={true}
summary={(i) => `${i.last_name}, ${i.first_name}`}
pathSegment={(i) => i.last_name}
pathSeparator=" > "
maxPathSegmentLength={10}
/>
First import the Htable component, and if you need to, also import the CaptionOrder
and the ItemGrouping
enumerations, which are used to control the order of things in sub-tables' captions as well as item ordering in the
tables (not shown in the example).
IMPORTANT This quickstart example will come out without styles. It is up to you to provide the needed styling.
If you haven't realized yet, your data needs to follow a specific structure. Each item is expected to have id
and
subItems
properties as per the definition of the Item
data type. Data items with a non-empty array in their
subItems
property will generate a sub-table whose rows will be generated from the data items found in said array.
There are two ways to style the tables generated by svelte-htable
. The most straightforward approach would be to
target the <table>
element using a regular CSS file (so the classes are not scoped to a component), or a Svelte
page/component file using :global()
to avoid Svelte from scoping the CSS. Then the table automatically picks the
styles up as per the usual CSS rules.
The second way is to apply a CSS class to the component, declaring its styles either in a regular CSS file or a Svelte
page/component file with :global()
. I would say this is probably the saner option because most likely you don't
want to target other tables in your project by mistake.
This should cover the basics for styling, but the component allows finer targetting of its internal parts.
All sub-tables are decorated with the sub
CSS class and also inherit the parent's CSS class. This means that one
can write CSS selectors of the form table.sub
|table.someClass.sub
(or :global(table.sub|table.someClass.sub)
) to
only target sub-tables. The screenshot shown in this ReadMe used this technique to apply sub-tables a different text
color. Something like this:
table.data {
/* Main table and sub-tables obtain this background color because they all inherit the data class. */
background-color: darkblue;
}
/* But wait! We want a different background color for sub-tables. */
table.data.sub {
background-color: darkgreen;
}
NOTE: I hope those color names exist! :-)
This CSS will grant the main table a dark blue background, while sub-tables will gain a dark green background.
But maybe this is not good enough. Maybe we want alternating background colors. No problem. Sub-tables also get
either the sub-odd
or sub-even
CSS classes depending on whether their level is odd or even. This can be used to
accomplish alternating per-table styling. The image shown at the beginning was created using these clases to apply
alternating greenish backgrounds to sub-tables. Hopefully this doesn't need an example? I'll just mention a sample
CSS selector: table.data.sub.sub-odd
.
Finally, the sub-X
CSS class is applied to the tables, where X
is the table's level. The main table is level 1,
while all immediate sub-tables are level 2. The main table has no extra CSS classes; the immediate sub-tables will
have the sub
and sub-2
CSS classes. This CSS class should help you style exact-level tables.
Sub-tables can show a caption on top with up to two pieces of information: The table's level and the item's path value. Since this is a table that displays hierarchical data, the concept of a path should be natural, and this is what is meant to be shown in the table's caption. See the next section for information about this and other features.
The table's caption (a <caption>
HTML element) is made up of two <span>
elements inside an inline-flexed <span>
element. Each of these are assigned a CSS class based on their position. The one to the left has the cpt-l
CSS
class; the one to the right, cpt-r
. Use these classes to style the pieces of information shown in the caption.
NOTE: What's shown to the left or the right depends on the value of the
captionOrder
prop as explained in the next section.
The expansibles are <details>
HTML elements. As per their usage, what is shown when the contents are collapsed is
inside a <summary>
HTML element. You may style by targetting this element. This is an example CSS that assumes the
tables have been applied the data
CSS class:
table.data details > summary {
/* Add the CSS for what is visible when the elements are collapsed. */
}
This <details>
element spans all table columns. The row that contains the element is further applied a sub
CSS
class to help styling. An equivalent to the above could be:
table.data tr.sub details > summary {
/* Add the CSS for what is visible when the elements are collapsed. */
}
This is more specific than the previous example, so this should help in case specificity comes into play.
The most common need is to style a table's rows in an alternating fashion. svelte-htable
covers this case by adding
either the odd
or even
CSS class to every row in the <tbody>
element of the table. Style targetting these CSS
classes.
The following is the complete list of supported props:
Property | Type | Description | Example |
---|---|---|---|
columns |
Column[] |
Required. Column definitions. | [{ key: 'id', name: 'ID'}] |
items |
Item[] |
Required. Data items. | [{ id: 1, name: 'John', subItems: [{ id: 2, name: 'Clara' }]}] |
class |
String | Optional. Used to apply CSS class(es). | "my-css-data-table" |
level |
boolean | ((level: number) => string) |
Optional. When using a Boolean value, it shows or hides the level in caption; when providing a function, it is used to create what shows up as Level in the caption. | true ; l => 'Level: ' + l |
showPath |
Boolean | Optional (defaults to false ). Set to true to show the sub-tables' paths in their captions. |
true |
pathSeparator |
String | Optional (defaults to '.' ). Divider used to join path segments to create sub-tables' paths. |
' > ' |
captionOrder |
Number (CaptionOrder ) |
Optional (defaults to CaptionOrder.PathLevel ). Specifies which data goes where in sub-tables' captions. |
CaptionOrder.LevelPath |
grouping |
Number (ItemGrouping ) |
Optional (defaults to ItemGrouping.ExpansiblesFirst ). Specifies how to group data items based on the existence of sub-items in them. |
ItemGrouping.ExpansiblesLast |
summary |
string | ((item: Item) => string) |
Optional. Used to control what is shown as the expansible item's summary. | i => i.name |
pathSegment |
string | ((item: Item) => string) |
Optional. Used to define sub-tables' paths. | i => i.employeeId |
maxPathSegmentLength |
Number | Optional. If specified, path segments that exceed this length will be truncated. | 10 |
initialOpenLevel |
Number | Optional (defaults to 1). Assign a number greater than one to expand items with tables equal or less than the specified number. | 5 (will show table levels 2 through 5 as open) |
nullText |
String | Optional (defaults to (---) ). Sets the representation of null property values used by the default value-rendering function. |
'(null)' |
A sub-table's caption may display up to two pieces of data: The sub-table's level and path. One can specify on which
side of the caption each value appears using the captionOrder
prop, and whether or not they appear using the level
and showPath
props.
The captionOrder
prop accepts any value of the CaptionOrder
enumeration:
Name | Numerical Value | Description |
---|---|---|
PathLevel |
1 | The sub-tables' captions will show the table's path to the left and the table's level to the right. |
LevelPath |
2 | The sub-tables' captions will show the table's level to the left and the table's path to the right. |
A sub-table's path is automatically constructed by using the data item's first property as defined by the column
definitions, unless the pathSegment
prop is specified with either a data item's property name, or a function that
takes the data item as function argument and returns the actual path segment value to use. Each path segment is
joined with other segments using the value of the pathSeparator
prop. If not specified, the default path separator
is a period (.
). Any string can be used as path separator, but since the sub-table's caption's real estate is very
limited, try not to use long separators.
Because of this limited real estate in captions, and most likely because your data will be dynamic in nature and
therefore unpredictable, this component allows the specification of a maximum path segment length to help prevent
overrunning the available space for both the level and the path data. It works like this: A path segment's value is
obtained and measured against the value specified in the maxPathSegmentLength
prop. If the length exceeds the
specification, then the path segment is truncated and ellipsis (...
) is added at the end, so that the combined
length is equal to maxPathSegmentLength
.
The value of maxPathSegmentLength
must be greater than 2. The component doesn't check this in runtime; you'll just
see an error if you go below this value.
The level is just a measure of how deep the data has gone in terms of recursion. The top table is level 1, and as tables are recursively created, the level is increased by one. Tables with a level greater than one are called sub-tables and are granted special CSS classes as seen in the Styling the Table section.
Since the table can be used to display text in any language, svelte-htable
cannot possibly add any text to the
label by itself. So, in order to be able to display a simple string like Level: 2
, you must provide a function
via the level
prop. Sure, you can just set level={true}
and the level is displayed, but it will only display
the number, which turns out to be uninformative. Still, the use of this prop is simple enough:
<Htable {columns} {items} level={l => `Level: ${l}`} />
This does the simple trick of outputting the level labeled with the word Level.
It may be desirable to rearrange the tables' rows according to their nature (expansible vs. non-expansible), so this
component exposes the grouping
prop. This prop accepts any value of the ItemGrouping
enumeration.
Name | Numerical Value | Description |
---|---|---|
Undefined |
0 | The provided data items will be presented in the order they were provided. |
ExpansiblesFirst |
1 | The provided data items will be rearranged so the ones with sub-items will be shown first. |
ExpansiblesLast |
2 | The provided data items will be rearranged so the ones with sub-items will be shown last. |
The summaries are the the text shown for expansible items. Said items are represented with a <details>
HTML element
that spans all table columns and its content is configurable. As per the HTML specification, the first child of this
<details>
element is a <summary>
element.
The contents of the <summary>
element are what's displayed when the element is collapsed. It is called the
summary for obvious reasons. After the summary, the sub-table is shown.
There are two ways to control what is shown in the summary: By using the summary
prop, or by using the summary
slot.
This would be the preferred option. Use the named slot summary
to customize what is shown as the expansible conent.
This is taken from the test application in this repository:
<Htable
class="data"
items={buildData(data.data)}
{columns}
level={l => `Level: ${l}`}
showPath={true}
captionOrder={CaptionOrder.LevelPath}
summary={(i) => `${i.last_name}, ${i.first_name}`}
grouping={ItemGrouping.ExpansiblesFirst}
pathSegment={(i) => i.last_name}
pathSeparator=" > "
maxPathSegmentLength={10}
>
<svelte:fragment slot="summary" let:item>
<img class="flag" src="https://flagcdn.com/{item.country_code.toLowerCase()}.svg" alt={item.country_code} /> {item.last_name}, {item.first_name}
</svelte:fragment>
</Htable>
A Svelte fragment is being used to set the slot's content to an image and the employee's name in the
<last name>, <first name>
format. This is what you see in the example screenshot at the beginning of this document.
NOTE: The slot itself provides the item currently being rendered.
The example also shows the use of the summary
prop. When both are specified, the slot version will win.
The other way to set the the summary would be to use the summary
prop. It can be a string, in which case it is
assumed to be the name of a property in the item, or it can be a function that accepts the item and returns whatever
you want as text in the summary.
If the example above did not use the summary slot, the function you see in the summary
prop would be used, creating
a very similar summary, only this one would not contain the image the summary-slotted version produces.
It is sometimes necessary to format data because the raw data might not be very friendly. For example, dates in the
ISO-8601 format are not much of a looker. To account for this, column definitions may define a renderValue()
function that is called to obtain the column's contents. If the function is not defined, svelte-htable
provides a
default function that only does 2 things:
- Undefined values are exchanged with empty strings.
- Null values are represented as
(---)
.
The null string is actually configurable through the nullText
prop. If you don't like this default, just set a new
one using this prop.
All value-rendering functions are given 2 arguments: The row's item, and the column's key.
IMPORTANT: Any empty strings are replaced by the
datacell
slot's default template with a non-breaking space (
) to ensure cell formatting, and you should too, if you opt to use thedatacell
slot. To assist with this,svelte-htable
exports theStdCell
component with this logic. See details here.
The renderValue()
function can be used to create calculated columns. The example screenshot at the top shows the Age
column, which is calculated by using the following column definition in the columns
array:
{
key: "age",
title: "Age",
render: (i, k) => {
const diff = Date.now() - Date.parse(i.birth_date);
return Math.floor(
diff / (365 * 24 * 60 * 60 * 1000)
).toString();
},
},
The renderValue()
method will only take you so far, and if complex HTML is needed inside the table cell, then you
must opt to use the datacell
or datarow
slots.
Tip: You can, and is encouraged to, combine the use of
renderValue()
with thedatacell
ordatarow
slots.
The Htable
component offers two mutually-exclusive slots to render data: The datarow
slot, and the datacell
slot. The latter is highly recommended because the former will incur in loss of certain features.
Dynamic slots don't seem to be a thing (yet) in Svelte, so there is a single slot for all columns, and in order to
use it, one must use an {#if}
block to differentiate between table cells.
This following example is what has been used to create the example screenshot:
<svelte:fragment slot="datacell" let:item let:col let:renderValue>
{@const value = renderValue(item, col.key)}
{#if col.key === "id"}
<div class="inverted">{value}</div>
{:else if col.key === "country_code"}
<a
href="https://flagpedia.net/{item.country_code.toLowerCase()}"
target="_blank">{value}</a
>
{:else}
<StdCell itemData={value} />
{/if}
</svelte:fragment>
The slot provides 3 pieces of data: The row's item, the column definition object and the column's renderValue()
function, but it is not exactly the one found in the column definition object. While column definitions may not
carry a renderValue()
function (and therefore col.renderValue
may very well be undefined
), this slot-provided
function is guaranteed to exist. How? Simple: This is the defined renderValue()
function inside the column
definition object, or the default renderValue()
function explained in the previous section.
The example also features the use of the StdCell
component. Let's quickly explain it.
This is a tiny component that has only one prop: itemData
. It encapsulates the logic mentioned previously about
exchanging empty strings with
. Its use is super simple:
<Htable
...
>
<div class="my-class" slot="column" let:item let:col let:renderValue>
{@const itemData = renderValue(item, col.key)}
{#if somethingSpecial}
<SpecialMarkup />
{:else}
<StdCell {itemData} />
{/if}
Basically we use it as a default.
If you ever need to gain control of how many columns a <td>
element spans, or if you must apply CSS classes to the
<tr>
element, you may opt to use the datarow
slot.
WARNING: The use of this slot carries the loss of stock features. Keep reading to learn about this.
Because you gain full control over the <tr>
element, the Htable
component can no longer provide the following
functionality:
- Automatic
odd
/even
CSS classes in the<tr>
element. StdCell
logic in the<td>
elements.
Furthermore, you must provide the <tr>
element as the slot's root element.
Still, your gains are evident. Now you can:
- Apply any styles or attributes to the
<tr>
element because you fully control it. - You may write the
<td>
elements more naturally, without the need of{#if}
blocks to switch between the various HTML markups for each of the table columns. - You gain full control of the
<td>
elements, including but not limited to, the ability to make it span mutliple columns.
This example creates the same table as the previous example, but making use of the datarow
slot:
<tr slot="datarow" class:flag={item.country_code === flagCountryCode} let:item let:index let:renderValue>
{#each columns as col}
{@const value = (col.renderValue ?? renderValue)(item, col.key)}
<td>
{#if col.key === "id"}
<div class="inverted">{value}</div>
{:else if col.key === "country_code"}
<a
href="https://flagpedia.net/{item.country_code.toLowerCase()}"
target="_blank">{value}</a
>
{:else}
<StdCell itemData={value} />
{/if}
</td>
{/each}
</tr>
Yes, I know, I still used {#each}
+ {#if}
to create the columns. I did not feel like re-writing this for 2
special columns out of possible 8. But that's not the point here, so focus.
The point of the example is that now we can manipulate <tr>
. In this case, a CSS class named flag
is
conditionally applied to the row to produce a highlight effect based on a variable (flagCountryCode
) bound to an
input textbox.
What is shown in the tables' headers is the text of the property title
of the individual Column
objects passed to
the component via its columns
prop.
However, if you need or want, you may opt to use the headercell
slot. It provides the column definition object and
it is pretty much very simple to use. This next example assumes the column definition objects also have a custom
property named uom
(stands for Unit of Measure).
<Htable
...
>
<svelte:fragment slot="headercell" let:col>
<em>{col.title} ({col.uom})</em>
</svelte:fragment>
This would produce a header with a text similar to, say, Distance (km)
.
You can control which sub-tables are initially displayed using the initialOpenLevel
prop. Assign a number greater
than 2 to this prop and any sub-tables whose level is that number or less will be expanded by default.
You may also listen to the toggle
event. It is triggered whenever any sub-table is opened or closed. The event
carries the following data:
item
: The data item used to generate the sub-table (the one with the sub-items).level
: The sub-table's level.path
: The sub-table's path.open
: A Boolean value indicating if the table was opened (and its current state is open).
If you have questions, would like to report a bug or request a feature, feel free to open an issue.