Forem

Ivan
Ivan

Posted on • Edited on

2 1

Easy apps with hyperHTML — 6, Customizing my custom elements

Part 6 written by Trevor Ganus,

pinguxx image
paritho image
  1. Introduction, wire/bind
  2. Events and components
  3. Moar about components and simple state management
  4. Wire types and custom definitions (intents)
  5. Custom elements with hyper
  6. Customizing my custom elements
  7. Testing!
  8. Async loading, placeholder and a Typeahead with hyper
  9. Handling routes
  10. 3rd party libraries

Recap

In parts 1–5, we talked about the basics of hyperHTML. We also learned about an amazing feature called hyper.Component. We dove into “intents ” that let us extend hyperHTML and simplify our templates. While learning all this we created a simple table:

Finally, we learned the basics of custom elements and a nice little utility called hyperHTML-Element, which gives us some of the hyper.Component features out of the box, and so much more.

In part 6, we’re going to discover how we can convert our table from a component to a custom element. We’ll also see how easy it is to add functionality to our custom element, extending it to be even more useful. Using custom elements makes it so easy to reuse our code, a baby could do it — if the baby knew HTML, and JavaScript, of course. Also, once we have a custom element, we don’t need hyperHTML anywhere else in our app unless we want it. This makes the custom elements plug-and-play with any framework. When was the last time a React component could say that?

Columns

Let’s start thinking about the elements of the table: header, footer and body. For our case, header and footer are the important parts. We will generate the footer based on the data just like before.

Let’s start with a custom element for the columns. This element will contain the information necessary to render:

  • the label (for the header)
  • the attribute used for this column (from the data)
  • if it’s sortable or not
  • the type of data to display

Simple right? We don’t even render anything, all the heavy lifting will be in the table itself.

Table

Now let’s generate a custom element with a simple table.

Let’s first add a hyper-column to our HTML

<hyper-table>
<hyper-column label="ID" attribute="id"></hyper-column>
</hyper-table>

Then, we need to update our created function to read the columns and store them in an array that we can reuse later:

created() {
this.columns = [].slice
.call(this.querySelectorAll("hyper-column"))
.map(el => {
const obj = {};
[].slice
.call(el.attributes)
.map(attr => {
obj[attr.name] = attr.value;
});
return obj;
});
this.render();
}

(used [].slice.call instead of […this.querySelectorAll(“hyper-column”)] because of an error using stackblitz)

And lastly, let’s update our template to use the columns.

render() {
this.html`
<table class="table table-striped table-bordered table-sm">
<thead>
<tr>
${this.columns.map(column => wire(column)`
<th>${column.label}</th>
`)}
</tr>
</thead>
<tbody>
<tr>
${this.columns.map(column => wire(column)`
<td>${column.attribute}</td>
`)}
</tr>
</tbody>
</table>
`;
}
view raw table-render.js hosted with ❤ by GitHub

Well cool. But we are not really using data from anywhere… let’s fix that. We’ll add a data property.

get data() {
return this._data;
}
set data(data) {
this._data = data;
this.render();
}

We now need to update our <tbody> part of the template to show one row for each item in the data.

<tbody>
${this.data.map(item => wire(item)`
<tr>
${this.columns.map(column => wire(column, `:col${item.id}`)`
<td>${item[column.attribute]}</td>
`)}
</tr>
`)}
</tbody>
view raw table-tbody.js hosted with ❤ by GitHub

(if wire(column, `col${item.id}`) looks unfamiliar to you, review part 4!)

Of course, we still need to pass data to the table. The index file is where we’ll do that.

import "document-register-element";
import "./Table.js";
const table = document.querySelector("hyper-table");
table.data = [{ name: 'John', id: 1, date: 432363600000 }, { name: 'Mitch', id: 2, date: 462777700000 }, { name: 'Paul', id: 3, date: 477368800000 }];
view raw index.js hosted with ❤ by GitHub

Let’s see our full code so far:

To finish off the table, we need to add the rest of the columns. Notice, this is very similar to what we’ve done in previous parts of this tutorial!

<hyper-table>
<hyper-column label="ID" attribute="id"></hyper-column>
<hyper-column label="Name" attribute="name"></hyper-column>
<hyper-column label="Date" attribute="date" sortable="false" type="date"></hyper-column>
</hyper-table>
view raw table-all.html hosted with ❤ by GitHub

Intents

Recall intents from part 4? Let’s use intents to display icons and dates correctly. We happen to like the octicon library of icons, but you can feel free to bring your own icon set.

import HyperHTMLElement from 'hyperhtml-element/esm';
const {hyper} = HyperHTMLElement;
import octicons from 'octicons';
hyper.define('oct-icon', (node, icon) => {
node.innerHTML = ' ' + octicons[icon].toSVG();
return icon;
});
hyper.define('date', (date) => {
let formatted = '',
newdate = date;
if (date) {
if (typeof newdate.getMonth !== 'function') {
newdate = new Date(date);
}
if (typeof newdate.getMonth === 'function') {
formatted = `
${newdate.getMonth() + 1}/${newdate.getDate()}/${newdate.getFullYear()}
`;
}
}
return {
text: formatted
}
});
view raw table-intent.js hosted with ❤ by GitHub

Then extract the rendering of the <td>’s and <th>’s for easier reading.

_renderCell(item, column) {
const obj = {};
obj[column.type || "html"] = item[column.attribute];
return wire(column, `:col${item.id}`)`
<td>${obj}</td>
`;
}
_renderHeaderCell(column) {
if (column.sortable == null || column.sortable === "true") {
return wire(column)`<th>
<a data-target="id" href="#">${column.label}</a>
${this.state.sorted === column.label ?
wire()`<span oct-icon=${this.state.asc ? 'chevron-up' : 'chevron-down'}></span>`
: ''
}
</th>`;
} else {
return wire(column)`<th>${column.label}</th>`;
}
}

(Note that, for user-provided values, you should be sure to use the ‘text’ type. Otherwise, use of ‘html’ could lead to potential HTML injection issues. Normally, hyperHTML will escape HTML used in expressions ${...}).

We are using state so let’s make sure we have a defaultState set:

get defaultState() {
return {
sorted: "",
asc: false
}
}
view raw table-state.js hosted with ❤ by GitHub

And again update our template

<thead>
<tr>
${this.columns.map(col => this._renderHeaderCell(col))}
</tr>
</thead>
<tbody>
${this.data.map(item => wire(item)`
<tr>
${this.columns.map(column => this._renderCell(item, column))}
</tr>
`)}
</tbody>

Much easier to follow :D.

State & Events

We need to be able to sort the data when a user clicks on the table header. Let’s add onclick="${this}" data-call="headerClick" to our a tags so we can do this. We will also need to add the headerClick function to handle the event.

headerClick(e) {
e.preventDefault();
const tag = e.target;
const attr = tag.dataset.target;
let asc = this.state.asc;
if (this.state.sorted === attr) {
asc = !asc;
}
if (this.state.sorted !== attr) {
asc = true;
}
//simple sort, reverse the sort if asc is true
this.data.sort((a, b) => (''+a[attr]).localeCompare(''+b[attr]) * (asc ? 1 : -1));
//update the sorted attr
this.setState({
sorted: attr,
asc: asc
});
}
view raw table-sort.js hosted with ❤ by GitHub

Remember calling setState will call render(), so we don’t need to explicitly call render() here. Let’s see our table with the sorting and icons working.

Footer

Then at the bottom of the table, we will add our footer.

view raw table-footer.js hosted with ❤ by GitHub

Nothing fancy here. Aaannddd we are done… or at least we replicated the component we had with custom elements. This is no small thing. Now, we can just use <hyper-table> with some <hyper-column> inside and you have a simple yet functional table.

Customization

Every good table should be able to add/update/remove columns, right? Glad you asked! Let’s add some functions to our table that will allow us to do just that.

First let’s add to our column element a new attribute called name. This will be used to update/remove columns. If no name is passed we’ll just use the attribute.

addColumn

We will let the developer add a column by passing a new <hyper-column>. We’ll also allow an object to be passed with the attributes needed. This will update our columns array.

addColumn(ElOrObj, ix) {
const obj = {label: "", attribute: "", sortable: undefined, type: undefined};
if (ElOrObj.nodeName === "HYPER-COLUMN") {
[].slice.call(ElOrObj.attributes)
.map(attr => {
obj[attr.name] = attr.value;
});
} else {
Object.keys(obj).map(key => obj[key] = ElOrObj[key] || undefined);
}
if (ix) {
this.columns.splice(ix, 0, obj);
} else {
this.columns.push(obj);
}
this.render();
}
//you will use it like this
const table = document.querySelector("hyper-table");
table.addColumn({label: "Last", attribute: "last"}, 2);
//or
table.addColumn(wire()`<hyper-column label="Last HTML" attribute="last" />`);
view raw table-addcol.js hosted with ❤ by GitHub

removeColumn

Based on the newly added name to the column, we can easily remove columns from the table. In theory we could use index instead of name, but I wanted to show that we can do either with the elements.

removeColumn(name) {
this.columns = this.columns.filter(col => col.name !== name);
this.render();
}
//use it like this
const table = document.querySelector("hyper-table");
table.removeColumn('lasthtml');

updateColumn

Again using the name attribute, we can update the column details.

updateColumn(name, attribute, value) {
const obj = this.columns.find(column => column.name === name);
if (obj) {
obj[attribute] = value;
this.render();
}
}
//use it like this
const table = document.querySelector("hyper-table");
table.updateColumn('last', 'label', 'Last Name');
view raw table-update.js hosted with ❤ by GitHub

Let’s see how it looks:

Well, that was easy, and we enhanced our table with very useful functionality!.

Now you are a custom element/hyperHTML master :D. Or, at least you can start creating your own elements! In the next part we are going to add testing to our table element and maybe add the ability to filter the table display. Please send us your feedback to improve this tutorial.


Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up