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
.
touch packages/component/src/table/Tr.ts
Open Tr.ts
in your IDE and begin developing the component by importing the Component
decorator from @in/common
.
import { Component } 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.
export class TrComponent extends HTMLTableRowElement {
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.
custom: { extends: 'tr' },
In TableCard.stories.js
import TrComponent
from Tr.ts
.
import { TrComponent } from "./Tr";
Export TrComponent
for the Storybook Webpack configuration.
TableCardComponent,
Set up TrComponent
the same way. Start by making a new file named Td.ts
in the same directory.
touch packages/component/src/table/Td.ts
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
.
import { Component, attachTemplate, attachStyle, html, css } from "@in/common";
Export a class named TrComponent
that extends HTMLTableCellElement
. Call super()
in the class constructor
as well as attachTemplate
and attachStyle
.
export class TdComponent extends HTMLTableCellElement {
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.
export class TdComponent extends HTMLTableCellElement {
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
.
<in-texinput type="text"></in-texinput>
Before proceeding with styling TdComponent
, head back on over to TableCard.stories.js
and import TextInputComponent
into the file.
import { TextInputComponent } from "../input/TextInput";
Export TextInputComponent
from TableCard.stories.js
to satisfy Storybook's Webpack compilation.
TableCardComponent,
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
.
padding-left: var(--padding-sm);
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
.
return this.querySelector('.td-readonly');
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
.
this.$readOnly.innerText = value;
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.
static get observedAttributes() {
Declare the attributeChangedCallback
method. While using a switch
statement, watch for the case
of value
and call setValue
there.
attributeChangedCallback(name, prev, next) {
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
.
this.$inputContainer.setAttribute('hidden', 'true');
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.
attributeChangedCallback(name, prev, next) {
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.
const tr = document.createElement("tr", { is: "in-tr" });
const td = document.createElement("td", { is: "in-td" });
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.
td.setAttribute("value", rowData[colData.property]);
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.
new CustomEvent("data", {
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.
public $rowData: any;
Import Listen
from @in/common
to set up a new event listener.
import { Component, Listen } from "@in/common";
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
.
this.$rowData = ev.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
.
const cells = this.querySelectorAll('td');
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.
import { TrComponent } from "./Tr";
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
.
return Array.from(this.querySelector('tbody').querySelectorAll('tr')).map(
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.
Get unlimited access to Fullstack Web Components, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
