The web components specification provides only a small part of the process for creating and using components, including methods like constructor, connectedCallback, disconnectedCallback, and a few others. The rest of the work falls on the shoulders of developers. Unfortunately, this "rest" is vast, and the creators of web components have defined nothing beyond this small portion. Without a guiding framework, developers may not always know how components should behave.
The KoiCom library offers a behavioral pattern similar to that of VCL components. This behavior is based on the constructor, the order of rendering, and events.
Constructor
The component's constructor is the constructor of the component's class. During the execution of the constructor code, the component may not yet be present in the DOM document. In other words, no external renderings are available to the component. As a result, some developers might think that the constructor is useless, while others might feel the urge to include as much code as possible in the constructor.
However, as we've seen in the sections on Inheritance and Composition, the component's constructor is closely tied to how the component defines its methods. Methods inherited or added via mixins are accessible within the constructor. Methods added through the "has-a" composition pattern are only available after the corresponding object is created. Therefore, the constructor is the best place to instantiate data objects and display objects.
In the KoiCom library, the constructor has a special significance. It is the method where the component's workflow begins, divided into stages, with each stage requiring the component to perform a specific set of actions. To emphasize that a certain set of methods is invoked during the constructor's execution, KoiCom uses the method _onConstructed rather than the constructor method.
If the component implements a "has-a" composition, the _onConstructed method must be overridden in the component. The overridden method should start by calling _onConstructed from the superclass to ensure that the component implements not only its own compositions but also all compositions from its ancestors.
The result of composition is usually the creation of a component attribute. In the example above, the attribute this.data is created.
In the KoiCom library, there are many pre-created behaviors. These behaviors create attributes, some of which include the following:
id | The component's identifier. |
---|---|
_state | The component's state object. It can have the following values: 'initializing', 'loading', 'badconnection', 'forbidden', 'ready', 'error'. It determines the availability of the component's methods and data for use. |
data | The component's own data object. |
_debug_mode | The component's debug mode flag. In debug mode, the component logs method names to the console in the order they are called. |
_changed_event_details | The data sent in the event. |
_changed_event | The event object triggered by the component. |
socket | The display object. It stores the identifiers and references to internal components. It manages the component's appearance. |
_connector | The connector object for external components. It allows retrieving data from external components. |
Since your tasks may require additional behaviors, you will create custom attributes for components. To initialize these attributes with certain initial values, you may need to retrieve these values from somewhere.
Although the component has not yet been inserted into the DOM during the constructor's execution, the component's tag attributes are already accessible within the constructor. That is, you can call getAttribute, which will return the value of the tag attribute. This allows you to gather attribute values for the component's attributes from the tag attributes. This will be explained in more detail in the "Attributes" section, but for now, here is an example.
In this example, the behavior KoiChangedEventMuteable defines the component's _muted attribute and sets its initial value from the muted tag attribute in the component's constructor, i.e., in the _onConstructed method. This approach is used throughout the KoiCom library.
The only exception is the process in which default values for the component's own data are gathered from the tag attributes. This process is moved out of the constructor into connectedCallback, which will be discussed below.
connectedCallback
If you connect a component as described in the Usage section, the connectedCallback is called after the constructor. This is where the interesting part begins, as the component is now embedded in the DOM and can be rendered.
In the Web Components specification, connectedCallback is not divided into stages, which leads many developers to write large blocks of code within this method without considering breaking it into smaller sections and organizing it.
In the KoiCom library, the process is divided into three stages: _onBeforeConnected, _onConnected, and _onAfterConnected.
The purpose of dividing it into stages is solely to separate and organize the initialization process of the connected component.
- In the _onBeforeConnected stage, the component prepares itself for operation.
The component retrieves default values for its own data, prepares its data for further use, and creates the event object.
Finally, the component creates the display object, which is used to render the template in the DOM and collect references to internal components. - In the _onConnected stage, the component sets its state based on its own data and can provide values for any part of its data that were not set in the previous stage.
- In the _onAfterConnected stage, the component attempts to display data in its template that is not expected to change during the component's lifecycle and updates its display based on data that can change.
If something prevents the component from being displayed at this stage, it will show an error message. For example, if the component's data is not filled out, and the component is expecting user input, it may notify the user about it.
Next, the component triggers its own events, signaling that the connectedCallback process is complete.
Finally, the component subscribes to events from its internal components and the connector.
As you can see, each stage has its specific purpose, and they should occur sequentially.
disconnectedCallback
To complete the lifecycle, the component should unsubscribe from all event subscriptions. To achieve this, the _onDisconnected method is called within the disconnectedCallback. By overriding this method, you can unsubscribe from the subscriptions that were added in _onAfterConnected.
Furthermore, if necessary, the disconnectedCallback should also release memory from objects created elsewhere in the component's code.
Events
Events play a vital role in the interaction processes between components. Interestingly, the concept of events has changed with the development of user interfaces and software. In the VCL documentation, it is already stated that events allow the developer to customize the component's behavior. Previously, in the Turbo Vision documentation, an event was defined as an occurrence that the application should respond to. The meaning of an event itself has evolved.
In Turbo Vision (TV), an event was something external to the component, whereas in VCL, there are both external and internal events. By writing the code for methods triggered by an event, the developer defines the behavior of the component, rather than just its reaction. In the KoiCom library, events are understood in this way. All actions carried out by methods with the on prefix, attempt, and even the constructor are, in essence, events. By overriding methods in subclasses of base components, the developer defines the component's behavior.
However, there are significant differences between events. When a component is inserted onto a page, the handling of the constructor and connectedCallback occurs sequentially, and after that, the component does not call methods that were invoked at these stages. In other words, there are events with a deterministic order. The response to these events is also predefined, and there is no need for additional objects to trigger one method in response to such an event. It suffices to call the response method within the event method itself. Typically, the terms "response" and "event" are not used when one method calls another.
However, in addition to such events, there are events whose trigger time is not predefined, and therefore, the response to them can happen multiple times at different moments. For example, the moment when a user presses a button is not predefined by the code. The program does not pause, waiting for the button to be pressed. To enable the program to react to the button press, additional objects and concepts are introduced. We say that a component subscribes to an event, that the component intercepts and handles the event. We introduce a special event object, a subscription method, and a handler method. Events of this type differ from events with a deterministic trigger order. Here and below, we will most often refer to such events.
These events are also distinguished by the complexity of their handling. At the moment of event interception, the component may be in various states and have different values for its own data. This creates the need to take into account the variability of the component’s state and data when handling events.
Can events be classified? Yes, events are typically classified. Events are assigned specific event codes. For example, most people are familiar with the click and change codes. However, this classification is somewhat meaningless. After all, what difference does it make to the component what exactly happened? What is more important for the component is what it needs to do. This leads us to the identification of a category of events, which can be conditionally referred to as commands. A component, API, or user issues a command that another component should perform.
Is it important who issues the command? If the handler of the command is a method that saves a document to a file, it doesn't matter whether the command to save the document was issued by a button, a menu item, or a component handling keyboard shortcuts. It turns out that the source of the command is not important.
But how should events be classified then? Let's recall that a component is a combination of behaviors. Each behavior defines its own set of reactions. The behavior that links the component to its own data defines the need to react to changes in the component's data. The behavior that links the component to the connector defines the need to react to changes in the connector's state. The behavior linking the component to an internal button defines the need to react to a button press. Since the method names within behaviors should not overlap to avoid overriding each other's methods, events in behaviors should be described by methods with different names.
Of course, this is not the strongest argument for separating events from one another, and I could have created the library without distinguishing between events, using a single universal type of event. However, I found that when events are described with distinct method names, which are additionally tied to behaviors, the resulting code is easier to understand.
Therefore, I have distinguished several types of events based on the behaviors. So, let's start with events that handle data changes.
Data Change Events
A component's own data can be changed for various reasons. Let's temporarily ignore the reasons for the change and who initiated it. Instead, let's focus on the subsequent events.
The KoiDataCapable behavior provides the method _updateSomethingWhenChanged, which is not tied to a specific event handler but can be invoked in combination with the _onAfterChanged method when the component’s own data is altered.
In the example given here, a method called attemptChangeValue is created, which can be used to modify the component's own data. This method first changes the data and then calls the _updateSomethingWhenChanged and _onAfterChanged methods.
The _updateSomethingWhenChanged and _onAfterChanged methods together, in this order, represent a response to the event of data modification. The first method updates the component's state based on the new values of the component's data, and the second, if the data has changed, calls the _handleSomethingChanged method.
What happens inside the _handleSomethingChanged method depends on the functionality of the component. If the component has a socket, it can update its display. If the component is a data provider, it can signal that its data has been modified.
What I just described is the process of handling the event of a component’s own data changing. Although the event has a deterministic nature, as we discussed earlier, it is accompanied by a processing procedure. This process follows a strict structure: first, the component’s state is modified, then the component updates its display and triggers its own events.
The process of handling an event for changing external data follows exactly the same structure. By "external data," we refer to the data of the provider.
Let’s assume a component acts as a data provider, and another component is the receiver of this data. For the second component to receive the data, the provider must signal the change. A few paragraphs earlier, we mentioned that the provider can signal a change in its own data using the _handleSomethingChanged method.
The KoiChangedEventDispatchable behavior enables the provider to do this.
Here, we see that the KoiChangedEventDispatchable behavior calls the _dispatchChangedEventWhenChanged method, which triggers an event through which the provider signals that its own data has changed.
The dispatchEvent method is a standard method that triggers an event created in the component as an object of the CustomEvent class.
This object is created with the code koi-changed, and from now on, I will frequently refer to the event object using this code. For example, I will refer to it as the koi-changed event.
By subscribing to the koi-changed event, an external component can find out that the component's data has changed and react to this change.
The usual purpose of the koi-changed event is to provide the data to the external component. In this sense, the component that triggered the koi-changed event acts as the data provider, and the component receiving the data should have a connector connected to the provider.
For a component to react to the koi-changed event from the provider, it must implement the KoiConnectorInteractable behavior, which determines the order of event processing.
Here, we again encounter familiar constructs like _updateSomethingWhen* and _onAfter*DataChanged. The event handling process described above is universal, with only the method names changing.
To summarize, we have a behavior that triggers changes to a component's own data. This behavior dictates the order in which the component responds to changes in its own data. If the component is a data provider, it uses the corresponding behavior, which enables it to trigger its own event after changing its data. Another component can intercept this event and respond to it. The event handling processes follow the same step-by-step structure.
Later, we will see that this pattern is used in the KoiCom library for handling all types of events, allowing us to quickly apply the necessary two behaviors to selected classes and create components that interact with one another, significantly accelerating development. For now, let's look at some details.
Providers and Event Handling
The first thing you may notice is the parameter in the _onAfterConnectorDataChanged method and its absence in the _onAfterChanged method. The reason for this is that, as I mentioned earlier, deterministic events do not require the creation of event objects. The handler for changes to a component's own data is aware of everything about the component’s state and can access the data directly. However, the handler for changes in the connector should not directly access the data provider. The provider must transmit both its state and its data through the connector, which is done using an event object. In this event object, you will notice the detail property, which combines information about the provider's state and its data. I use the terms "data received in the event" and "data passed in the event" to describe this phenomenon.
The second thing you may notice is the presence of the _updateSomethingWhenConnectorDataChanged and _onAfterConnectorDataChanged methods, but the absence of the onBeforeConnectorDataChanged method. This method is present in many frameworks, but in the KoiCom library, it is not used. Typically, this method contains code that validates data before it is used, preventing its use if the data is invalid.
But let’s think this through. If we prevent data from being processed, who is responsible for notifying the user that the data could not be applied? Since the component does not store invalid data, it doesn't know whether the data is invalid or not. Who else could take on the responsibility of notifying the user?
The responsibility can be transferred to the provider that supplied the data. The provider passed the data, so it possesses it. After transmission, it turns out that the data is erroneous in the context of the receiving component. However, the provider should not be aware of the context of the receiver.
Traditionally, error notifications are not displayed just anywhere; they are displayed in the component that received the data. This forms the basis for the receiver to store the erroneous data it received as its own and, based on that data, show an error notification.
In the KoiCom library, a component must accept any entered data and modify its state accordingly. This is why there are no onBeforeDataChanged or onBeforeConnectorDataChanged events. Data is always stored in the component, even if it is invalid. Then, the methods _onAfterDataChanged and _onAfterConnectorDataChanged are responsible for displaying the data and validation results, relying not on the transmitted data, but on the data and state of the receiving component. We will discuss this topic in more detail in the "Dataflow" section.
User Input Events
Non-deterministic user input events include events such as button clicks. All other events, such as entering a string in an input field, are handled in a similar manner. Therefore, I will describe user input events using the example of a button click.
Button components inherit from the KoiButtonStencil class. This class implements the KoiOperationsInterceptable behavior, which subscribes to events from the button's internal standard component and defines the order for handling the standard event.
As with data change events, the _attemptApply* method is called, which in this case is named _attemptApplyOperated. This method, like the previous one, first attempts to modify the state and the component's own data (via the _updateSomethingWhenOperated method) and then calls the _onAfterOperated method, which follows the same structure as all _onAfterChanged* methods.
The call to the _onAfterOperated method eventually leads to the invocation of _dispatchOperationEvent, where the standard dispatchEvent method is called to trigger the koi-operated event.
Therefore, the process for handling user input events is identical to the process for handling data change events. The only difference is the name of the behavior that must be used to intercept the user event.
Summary
A component's workflow consists of several processes. Some of these processes are deterministic, meaning methods are called one after another. An example of this is the component initialization process. Other processes are non-deterministic and may be triggered as a reaction to an event. An example of this is handling a button click.
The KoiCom library exposes both deterministic and non-deterministic processes. For example, the component initialization process is divided into several stages, each of which involves the component executing specific methods. In the constructor, the component initializes the composition of behaviors, and at the _onBeforeConnected stage, it retrieves the default values for its own data, and so on. This structure ensures that each subsequent stage can rely on the results of the previous one.
Non-deterministic processes are standardized. The reaction to any event follows a few sequential stages: changing the component's own data, its state, its display, and triggering its own event. This standardization allows the developer writing an event handler to be confident about how and when, and most importantly, in which method, the handling can be implemented.
Despite this standardization, events are classified, but not by the type of event initiator. Instead, they are classified according to the corresponding behavior of the component. This classification includes reactions to changes in the component's own data, changes in the provider's data, user actions, and so on.