Show Sidebar Menu

KoiCom Building Complex UI Elements

The article explains how web components can be used to create complex UI elements by inserting them into the DOM. It covers how components like KoiLabel and KoiPanel can be extended, combined, and managed using sockets.

Web components have one very important feature that will likely seem obvious to you. A component can be inserted into the DOM by simply inserting its tag.

For example, let's say we have an empty div.

Empty div

You can run the following command in the console and see an active instance of KoiLabel.

It seems like nothing unusual, but this feature allows you to create composite components. In other words, you can create complex components that provide sophisticated functionality. For instance, forms for user input that send data to the server, display errors, and show notifications, or lists that fully implement CRUD operations.

Panel Component

A typical composite component in many frameworks is a panel. In the KoiCom library, there is also a component called KoiPanel, which has this functionality. More accurately, it doesn't have any functionality beyond the basic one.

The panel itself has no template, so it is invisible. In order to see the empty white rectangle above, I had to populate the style and class attributes of the panel tag.

Although the panel lacks functionality, this component can serve as a parent for a wide range of composite components. Let's add a couple of buttons and a label to the panel. For this, we'll create a subclass of the panel — the DocsSampleDialog component.

Note that the DocsSampleDialog component's tag does not contain either the label or the buttons inside it.

The inner workings of a composite component are hidden in its template, where HTML is generated using the getTag methods, which contain the tags for the inner components.

Thanks to the ability to create components on the page by inserting HTML code, the composite component doesn't require additional functionality to create inner components. A composite component works just like any other component — it manages its own HTML code. The initialization of inner components is handled using built-in methods, and the developer doesn't need to create class objects or manage the timely calling of destructors.

Similarly, inner components can be added and removed not only in the _onBeforeConnected event. Inner components can be added and removed during the _updateAppearance call in the handler for any event.

Let's imagine a component that displays a list of database records. Each record has control buttons next to it. When transitioning to a new page of the list, the control buttons from the previous page are removed, and for each new record on the new page, new buttons should be added. The component can completely clear its innerHTML and refill it. The browser takes care of the work of turning the inserted HTML code into functioning button components.

The automatic transformation of a JS class into a component, a component into HTML code, HTML code into a component, and so on — this is one of the many things I like about web components. This automatic transformation is as obvious as it is surprising and effective.

Socket

A component's template is part of its display, and earlier (in the MVC chapter), we discussed how convenient it is to encapsulate everything related to the appearance inside a display object, which, in the KoiCom library, is called a socket.

The getTemplate method of the DocsSampleDialog component is actually contained within the DocsSampleDialogSocket socket.

The component interacts with the socket using the KoiSocketConnectable behavior, which provides two main methods: _displaySocket and _updateSocket.

The _displaySocket method is called once, after the component is first ready to display its data. In this method, you can render data in the template that will not change afterward. For example, you can render the table header.

The _updateSocket method is called whenever an event related to a change in the component's state or data occurs. In this method, you can render the consequences of those changes in the template. For example, you can render the table rows filled with data.

For simple components, like KoiLabel, this is sufficient. With each call to _updateSocket, you can redraw the entire contents. But for more complex composite components, like DocsSampleDialogSocket, there is no need to completely redraw the component template. It's enough to change the data for the label component while leaving the buttons untouched. This is where the need to work with internal components selectively arises.

Naturally, the methods for working with internal components should be implemented in the socket, as only the socket has the authority to decide whether to completely redraw the template or not. The component, acting as the controller, should not be aware of how the display will be implemented. Therefore, the socket should not only provide the necessary methods but also encapsulate the way the data is rendered. This leads to the need to implement not just handling of internal components but to encapsulate that behavior within the socket object.

Now let's look at the sets of internal components. Often, components only need one internal component to manage. For example, KoiLabel is a wrapper around a regular span and really only changes the text. For such components, KoiCom uses a socket of the KoiSingleSocket class. But there are also complex components containing many internal components. For example, any input form contains multiple input fields. For these components, the KoiCompositeSocket class socket is a better fit.

Most often, a component will have a single socket of either the KoiSingleSocket or KoiCompositeSocket class. However, there are cases when this is insufficient. In these cases, you can add as many sockets as needed to the component. For instance, a subclass of KoiSingleSocket can manage the table header, one subclass of KoiCompositeSocket can manage the table rows, and another subclass of KoiCompositeSocket can handle the buttons next to the table. This behavior can also be implemented.

References to Internal Components

The KoiSingleSocket and KoiCompositeSocket sockets attempt to access internal components immediately after the component’s template is rendered into the DOM. They do this by using references to the internal components. For instance, KoiSingleSocket has a _item property, which stores a reference to the single internal component it manages.

To enable KoiSingleSocket to access an internal component, the template must include an identifier automatically generated by the socket for the internal component’s tag. This identifier can be obtained using the getID() method.

For KoiCompositeSocket to manage its internal components, it must specify the identifiers of these components in the _getEmptySchemaIds method and reference these identifiers in the template.

KoiCompositeSocket stores references to its internal components in the protected _items property. To allow the parent component to interact with its internal components, the socket should implement methods that work with the internal components via the _items property.

As mentioned earlier, sockets automatically gather references to internal components after the template is rendered. However, since internal components can be added or removed not only when the template is first rendered into the DOM but also during the operation of a composite component, you might need to update the list of references to internal components dynamically. For example, in a table where each row contains an input field, changes to the table's content may require re-adding input fields and updating their references.

To handle this, after rendering a new set of components into the DOM, you need to directly update the list of internal component identifiers and then collect their references using the _addAndPrepare method.

Readiness of Internal Components

Many browsers execute the complete lifecycle of a component immediately after it is inserted into the DOM using innerHTML, without advancing to the next line of code.

For instance, in the previous example, the _insertComponentToDOM method triggers not only the insertion of a tag into the DOM but also the initialization of the component. Only after this does _addAndPrepare get executed.

However, in theory, a browser could delay the execution of the internal component’s constructor until after the composite component has completed the chain of methods responsible for creating internal components.

This might result in internal component methods being unavailable to the composite component immediately after the DOM insertion. For example, the following code has the potential to raise an error:

To avoid such issues, it is advisable to design composite components in a way that eliminates the need to call methods on internal components immediately after their insertion into the DOM.

Additionally, refrain from creating dependency chains where the composite component inserts two internal components into the DOM, and the first component triggers an event immediately after insertion. If the composite component’s event handler attempts to call a method on the second internal component, that second component might not yet be initialized, resulting in errors.

Lastly, some components may stop existing, lose the ability to provide data, or encounter other limitations.

To prevent such problems, it is recommended to utilize socket methods like _has(key) and isLinked. Moreover, in most cases, communication between the socket and internal components should rely on events rather than direct method calls on the internal components.

Events of Internal Components

Internal components can emit events that the composite component must handle. Typically, internal components are inserted into the innerHTML of the composite component. In such cases, the composite component can subscribe to events emitted by the internal components. If these events have the bubbles property of the CustomEvent class set to true, they propagate up the DOM and become events of the composite component itself.

In the following example, the KoiOperationsInterceptable behavior enables the component to intercept button clicks, eliminating the need to maintain references to these buttons.

The KoiOperationsInterceptable behavior subscribes the component to button click events in such a way that, when an event is intercepted, the component calls the _attemptApplyOperated handler. This handler follows a standardized structure shared across components.

In the handler, the event's further propagation is explicitly stopped by calling stopPropagation, ensuring that events from the internal components of the dialog panel are fully handled by the panel component itself. These events should not affect the behavior of any other components in the project.

This approach is the most common way to handle events from internal components. However, not all events from internal components have the bubbles property, and not all internal components are inserted into the composite component’s innerHTML. For example, a pop-up dialog box of a list may technically be an internal component of the list. Still, due to layout constraints, it must be added to the document's DOM just before the closing tag.

In such scenarios, event subscriptions for internal components must be established through direct references to these components. Since the composite component should not be aware of the internal component's structure, the subscription logic must be implemented by the socket.

In this example, we extend the click interception behavior from KoiOperationsInterceptable and implement event subscription methods. This approach delegates the responsibility of selecting the component that will intercept events to the socket rather than the main component.