Making the Table Editable

You'll learn about how to create a separation of concerns between the many custom elements that comprise the editable table.

In this section, you'll work on giving TableComponent the various modes required by the acceptance criteria: read-only and editable, while also giving the user the ability to add and delete rows. You'll further practice a separation of concerns by developing two new components: TrComponent and TdComponent that handle the functionality of a row and cell, respectively.

We used attribute drilling and BroadcastChannel in the previous section to communicate between components in the same layout. We'll add CustomEvent as a method for custom element communication in this section. You'll also learn how to dynamically create new instances of customized built-in elements with createElement and give custom elements data with HTML data attributes.

Separation of Concerns#

In the last section, you dynamically created HTMLTableRowElement and HTMLTableCellElement in TableComponent in response to receiving a message over BroadcastChannel. To further promote a separation of concerns, let's break out these two elements into new components. Otherwise, logic that should probably be contained by the HTMLTableCellElement will be leaking all over TableComponent. TrComponent and TdComponent will be customized built-in elements because we want them to retain the characteristics of the native DOM elements, while enhancing the element with more functionality. TrComponent will manage state for the entire row, while TdComponent will manage the view of the HTMLTableCellElement, whether it's in "edit mode" or "read-only mode", and set the value of the cell.

To start, make a new file in the same directory named Tr.ts. This file will contain the component logic for TrComponent.

Open Tr.ts in your IDE and begin developing the component by importing the Component decorator from @in/common.

Declare a new class named TrComponent and have it extend HTMLTableRowElement. Make the constructor and insert a call to super(). Ensure the class is exported using the export keyword.

Use the Component decorator to set the custom element tag name as in-tr, making sure to also provide the necessary custom property needed to register customized built-in elements.

In TableCard.stories.js import TrComponent from Tr.ts.

Export TrComponent for the Storybook Webpack configuration.

Set up TrComponent the same way. Start by making a new file named Td.ts in the same directory.

Import all the necessary API from @in/common. You'll need to style parts of a custom template in TdComponent so be sure to import attachTemplate and attachStyle in addition to Component.

Export a class named TrComponent that extends HTMLTableCellElement. Call super() in the class constructor as well as attachTemplate and attachStyle.

Set up the Component decorator, passing in the tag name as in-td to the selector property, the appropriate Object for registering the customized built-in element with the CustomElementRegistry. Leave the style and template properties blank for now.

TableComponent should display two modes: read-only and editable. In read-only mode, each TdComponent should display non-editable read-only text, while in edit mode the component should display a text input, more specifically TextInputComponent. Add two HTMLSpanElement to the template, tagging each with a unique CSS class name. In the span classed td-input, add an instance of TextInputComponent.

Before proceeding with styling TdComponent, head back on over to TableCard.stories.js and import TextInputComponent into the file.

Export TextInputComponent from TableCard.stories.js to satisfy Storybook's Webpack compilation.

Moving back to TdComponent, add styling for both the HTMLSpanElement you introduced in the template. We need to ensure the read-only content is styled per the spec in Figma, while also retaining the characteristics of a HTMLTableCellElement. To hide/show the read-only value, let's use a similar method to the buttons in TableCardComponent. Add a style for when the hidden HTML attribute is truthy. The element should be display: none.

Add getters for each of the elements you introduced to the layout: both HTMLSpanElement and the TextInputComponent. You'll need to reference each of these elements in the component logic you'll develop in TdComponent.

Make another method on TdComponent for setting the value of the read-only HTMLSpanElement. Pass an argument named value through the method and set value on the innerText of the HTMLSpanElement.

You'll need a way for TableComponent to communicate the value of the cell to TdComponent. Since you're already looping over HTMLTableCellElement in the context of TableComponent it seems appropriate to set the value via a HTML attribute. Listen for changes to a value attribute in TdComponent with the observedAttributes static getter.

Declare the attributeChangedCallback method. While using a switch statement, watch for the case of value and call setValue there.

We'll require another method for toggling the mode of the TdComponent from read-only to editable. Declare a method named setMode and pass an argument named readOnly, type defined as boolean, to an if statement. Depending on the truthiness of readOnly, hide or show the appropriate HTMLSpanElement.

Add readonly to the Array of observedAttributes and make a new case in the attributeChangedCallback, being sure to call setMode with the value of the readonly attribute. Since HTML attributes are String, we need to coerce the value of the attribute to a boolean because setMode expects a boolean. You can do this with the expression next === 'true', which will return a boolean that is truthy, otherwise falsy.

TdComponent is now set up to render different user interfaces based on the mode passed through the readonly HTML attribute. However; we still need to refactor TableComponent to accommodate the new customized built-in elements.

Rendering Modes#

In the last section, we dynamically created HTMLTableRowElement and HTMLTableCellElement with createElement in the renderRows method in TableComponent. Since then, we made these elements customized built-in elements and need to update the logic in renderRows to accommodate the change. createElement takes a second argument, an Object that specifies for the browser that these should be instances of a customized built-in element with a tag name set on the is property.

Update the calls to createElement in renderRows to create customized built-in elements with the tag names you declared earlier in this section.

The initial state of each table cell should be read-only as opposed to edit mode, so ensure each TdComponent has a call to setAttribute that sets the readonly HTML attribute to true on initial render.

Using CustomEvent to Transmit Row Data#

Thus far we haven't done much with TrComponent, even though the purpose of this component is to store row state. TrComponent is a direct descendant of TableComponent and in the renderRows method we already have a reference to each TrComponent. One of the requirements for using CustomEvent is being able to easily select the element. The main reason we've stuck with BroadcastChannel is because the deeply nested elements that required events were not immediately queriable from higher up in DOM. Even then, we needed to use attribute drilling so the nested TableComponent could listen to the same channel. Although this only required passing a String through HTML attributes and not a complex Object or Array. CustomEvent can be a viable means for component communication, especially when complex data has to be transmitted.

The Object that corresponds to each row in the loop is rowData. Use the dispatchEvent on HTMLElement to send a new CustomEvent named data, passing in rowData in the detail property.

Let's react to this CustomEvent in TrComponent before providing a method that displays each mode in TableComponent. In Tr.ts, add a new public property on the class called $rowData. This property should be publicly accessible so TableComponent can access it.

Import Listen from @in/common to set up a new event listener.

In TableComponent, you dispatched a CustomEvent to the instance of TrComponent, so let's declare a new event listener with Listen. Declare a new method named setValue that serves as the event listener callback method. Pass the event through the first argument ev and set the value of this.$rowData to the detail.

We'll circle back to TrComponent later, but for now, let's stick with managing the rendering of nodes in TableComponent. Make a new method on TableComponent named onReadOnly. the purpose of this method is to display the TdComponent in read-only mode. When the method is called, select all the cells with querySelectorAll and loop over the NodeList with forEach. On every iteration of the loop, set the readonly HTML attribute of each TdComponent to true.

Table Modes and State#

Read-only mode was fairly trivial. To set the display of each TdComponent, all you had to do was set the readonly HTML attribute. Edit mode is a bit more complex. After the user initiates edit mode by pressing the edit button, they have the option of editing each cell or canceling the edits they made. We need a way to store the state of the rows in the table when the user enters edit mode, in the event the user wants to revert the changes. This is why we went to the trouble of storing the data for each row on TrComponent, but we still need a way to store the state of all rows in the context of TableComponent. To get started, import TrComponent into Table.ts because you'll need to reference the $rowData property that exists on each instance of TrComponent in upcoming logic.

Declare a getter method on TableComponent that queries for all TrComponent in the TableBodyElement using querySelectorAll. We don't need an Array of TrComponent, but instead an Array of row data, so we need to use a method on Array.prototype not available to us on the NodeList. To access map on a NodeList you have to coerce the NodeList into an Array using Array.prototype.from.

This lesson preview is part of the Fullstack Web Components course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

Unlock This Course

Get unlimited access to Fullstack Web Components, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Fullstack Web Components