Show Sidebar Menu

KoiIdLink - Command-Oriented Buttons for Web Interfaces

This article discusses the KoiIdLink component from the KoiCom library, a button for issuing commands. It emphasizes event-driven design, decoupling button actions from logic for cleaner, maintainable code.

The purpose of the KoiIdLink component is to provide users with a means to issue a command, receive a command from the user, and transmit the command code within an event.

The KoiIdLink component is essentially a simple button styled to resemble a hyperlink.

Some Link

Typical use of native hyperlinks as buttons involves assigning actions using the onClick attribute of the link tag. However, this approach mixes HTML and JavaScript code, leading to less maintainable code.

Another common method of using native links as buttons is to bind an event handler to the link during initialization. In this case, after injecting the link into the DOM, you must use addEventListener to add the handler, monitor the moment the link is removed from the DOM, and then execute removeEventListener. These steps are nearly always repetitive and inefficient to implement for each button individually.

In addition to the above drawbacks, both approaches share another issue: the command handler triggered by the user and the button’s click handler are tightly coupled. Ideally, command processing should occur within the context of the application’s domain, not the button’s context. The method executed by the command should not need to know that it was triggered by a button, and the button should not need to know which method it triggers.

In the KoiCom library, buttons are not directly bound to handlers and do not execute commands via their own methods. Instead, the button's sole responsibility is to collect user input regarding the desired command and transmit this information via an event. Receiving the event and executing the command is the responsibility of another component, one that is aware of the command and acts as the controller.

When clicked, the KoiIdLink component triggers the koi-operated event and transmits its own data within it. For the base KoiIdLink component, this data consists of:

  • The identifier of a specific element,
  • A string code representing the action the user wants to perform on that element, and
  • An optional value, if the user wishes to include additional information when executing the action.

Together, these pieces of data describe the command.

Class

The initialization process of the component is as follows:

  1. _onConstructed() KoiDataCapable
    • _constructState() KoiStateCapable
    • _constructData() KoiButtonDataCapable
    • _constructSocket() KoiButtonNativeLinkSocketConnectable
  2. _onBeforeConnected() KoiDataCapable
    • _prepareDefaultDataValuesFromAttributes() KoiDataCapable
    • _prepareSocket() KoiSocketConnectable
      • socket.getTemplate() KoiButtonNativeLinkSocket
    • _constructOperationEvent() KoiOperationEventDispatchable
  3. _onConnected() KoiElementStencil
    • _updateSomethingWhenConnected() KoiElementStencil
      • _updateOwnDataWhenConnected() KoiElementStencil
      • _updateStateCodeWhenConnected() KoiStateCapable
  4. _onAfterConnected() KoiElementStencil
    • isSomethingChanged() KoiDataCapable
    • _handleSomethingChangedWhenAfterConnected() KoiBaseControl
      • _dispatchChangedEvent() KoiChangedEventDispatchable
      • updateAppearance() KoiBaseControl
    • _setNothingChanged() KoiDataCapable
    • _subscribeToEvents() KoiOperationsInterceptable
      • _subscribeToOperateEvent() KoiOperationsInterceptable

The KoiIdLink component inherits from KoiButtonStencil and implements the behaviors KoiButtonNativeLinkSocketConnectable and KoiButtonDataCapable.

The KoiButtonStencil class describes the process by which a component subscribes to events emitted by an internal native component. The _getInterceptableOperateEventCode method, defined in the KoiIdLink class, specifies that the KoiIdLink component intercepts native events of the click type. Upon intercepting such an event, without altering its state or data, the KoiIdLink component triggers its own koi-operated event, passing along the values of its internal data.

The KoiButtonNativeLinkSocketConnectable behavior specifies which native component will respond to user actions and serve as the source of events. For the KoiIdLink component, this native component is a hyperlink.

By replacing the KoiButtonNativeLinkSocketConnectable behavior with KoiButtonNativeButtonSocketConnectable, which defines a different socket for the component, you can create the KoiIdButton component. This modification expands the range of button states that can be displayed, thanks to the socket.

Some Button

The KoiButtonDataCapable behavior defines the structure of the internal data of the KoiIdLink component. This data is represented by a KoiOperationData object, which stores all three values describing the user command: the identifier, action code, and parameter.

The koi-operated event triggered by the KoiIdLink component has the bubbles property set to true. This allows an external component, which contains the KoiIdLink and acts as a controller, to respond to KoiIdLink events and receive the command specified in the button's internal data without needing to know about the button itself.

For a controller component to respond to button events, it must implement the KoiOperationsInterceptable behavior. This behavior subscribes the controller to the koi-operated event and enables it to handle the event in various ways.

Data

The internal data of the KoiIdLink component is of type KoiButtonData. The KoiButtonData class inherits from KoiOperationData, which defines data using the following schema:

Buttons are most often used to allow users to issue commands. Sometimes, a command is related to a specific element (e.g., a record in a table). To enable the button to transmit information about what command is being issued and concerning which element, the KoiOperationData object includes the fields item_action and item_id.

The item_action field can contain a string representing the command code. The item_id field can contain a string representing the identifier of the element.

Additionally, a command may include parameters defined by a specific value. For example, the command "update value" would need to include the new value itself. These parameters are similar to function arguments. To allow a user-issued command to include such a parameter, the data object has the item_value field, which by default is of type KoiDataElementString.

When a controller component receives an event, it can access the data object passed with the event and use the getAction method of the data object to retrieve the string command code.

To retrieve the values of the other command fields from the event, the data object provides the getItemId and getValue methods.

Attributes

id The identifier of the component.
item_id A string identifier of the item.
item_action A string identifier of the command.
item_value The value of the command parameter.
btn_class An additional CSS class for the button.
placeholder The text displayed on the button.

The KoiIdLink component has three attributes — item_id, item_action, and item_value — whose values serve as defaults for the data passed in the emitted event. These attributes are used to specify the user’s intent, i.e., to convey the purpose of the button press.

For instance, imagine displaying a list of database items. Each item's card includes a brief description of the item. To view more detailed information, the user can press a button on the item's card. The button must then communicate in the event which specific item the user wants to view in detail. The item's identifier is passed in the item_id field of the event data. When creating the list, the item_id is set via the button tag's item_id attribute.

It is important to note that the item_id is passed as a string. This is because database records do not always have numeric identifiers. Furthermore, there are scenarios where other types of identifiers, not database-related, need to be passed, depending on the button's purpose.

Now consider a case where a user can perform several actions with each item. Each item’s card might include multiple buttons, each corresponding to a specific action — e.g., view, edit, or delete. These actions all pertain to the same item, but the actions differ. Differentiation of these actions is managed using the item_action field in the event data, which is set via the button tag's item_action attribute.

Upon receiving a data object in the _attemptApplyOperated handler, the controller can extract the item identifier and the action code to determine what action the user wants to perform on which item.

The KoiIdLink component also has two additional attributes:

btn_class: A string attribute that allows assigning a CSS class to the a tag, which acts as the wrapper for the component.

placeholder: This is not exactly an attribute but rather the content of the tag, though it is passed as an attribute in the getTag method parameters. This attribute allows setting the text that should appear within the a tag.

State

The KoiIdLink component does not have any additional display states beyond those defined in the base class KoiBaseControl. These states are controlled by the methods show and hide.

However, it is possible to implement states where the link becomes active or inactive depending on its own data or the data of external components. This functionality is, for example, implemented in the KoiIdButton component, which is also a descendant of KoiBaseButton.

This feature is useful for cases such as implementing a "Save" button in text editors. The "Save" button is disabled when the text is already saved and becomes active when the text is modified and differs from the saved version. Similarly, a "Send" button on a data entry form can be disabled when data is missing or invalid and activated when the data is ready to be submitted to the server.

The KoiIdButton component can also display two additional states:

  • A spinner (hourglass) is shown to indicate that, for instance, data is being sent to the server.
  • The spinner is hidden when the operation, such as data submission, is complete.

The display state of the button is controlled through the methods enable, disable, showHourglass, and hideHourglass.

In the example below, when the input field is empty, the button is disabled. If any data is entered into the field, the button becomes active. If the entered data is subsequently erased, the button is deactivated again.

In this example, when the text in the KoiFormFieldString component is modified, it triggers the koi-form-field-change event. A panel subscribes to this event and adjusts the button's activity based on the value transmitted in the event. Essentially, the panel acts as a controller managing the button's state using the enable and disable methods.

Note: In this example, the panel is not a form. The example is intended to demonstrate how a composite controller component can manage the state of buttons. A form, on the other hand, implements a more complex mechanism for validating user-entered data and a more sophisticated process for managing its internal components.

Event

The KoiIdLink component has its own data but does not implement the KoiChangedEventDispatchable behavior, so it does not trigger the koi-change event when the data changes. Instead, the component triggers the koi-operated event when the button is clicked, i.e., when the user interacts with the component’s socket.

The koi-operated event object is of type CustomEvent and is created at the moment of onBeforeConnected in the _constructOperationEvent method, so that the event object is not created every time the event is triggered.

The koi-operated event has the bubbles property set to true, which causes the event to propagate upward through the DOM tree. This allows buttons to be created within a composite component, and the composite component will automatically handle their clicks, acting as a controller.

In this example, the buttons on the left trigger the koi-operated event, sending string command codes such as "add button" and "remove button" in the event data. The buttons created on the right generate a koi-operated event that passes their identifiers. The encompassing component acts as a controller, intercepting and handling the events. For this purpose, the encompassing component implements the KoiOperationsInterceptable behavior.

Upon receiving the command code, the controller component either modifies the set of buttons on the right or changes the text of the label located above the buttons on the left. The label text is set to the identifier passed in the event.

For the controller component to respond to the button events, it must implement the KoiOperationsInterceptable behavior, which subscribes it to the koi-operated event and allows it to respond to the event in various ways.

The choice of how to respond to the event depends on the implementation of the controller component. In one variant, the controller component has its own data and changes it based on the commands received in the events. Then, depending on changes to its own data, it updates its display and triggers its own events. In another variant, the controller component does not have its own data and reacts to the commands in the events depending on their contents. In a third case, the controller component has a more complex logic and chooses how to respond based on the commands it receives.

To ensure flexibility, the handling of the koi-operated event by the controller component is carried out as follows:

_attemptApplyOperated() KoiOperationsInterceptable
  • _isOwnOperateEvent() KoiOperationsInterceptable
  • event.stopPropagation() KoiOperationsInterceptable
  • _updateSomethingWhenOperated() KoiOperationsInterceptable
    • _updateOwnDataWhenOperated() KoiOperationsInterceptable
    • _updateStateCodeWhenOperated() KoiOperationsInterceptable
      • _determineStateCode() KoiBaseElement
      • _setStateCode() KoiStateCapable
  • _onAfterOperated() KoiOperationsInterceptable
    • isSomethingChanged() KoiStateCapable
    • _handleSomethingChangedWhenOperated() KoiOperationsInterceptable
      • _dispatchEventsWhenChangedAfterOperated() KoiOperationsInterceptable
      • _updateAppearance() KoiBaseControl
    • _setNothingChanged() KoiStateCapable
    • _handleOperated() KoiOperationsInterceptable

Firstly, the controller component blocks further event propagation by calling event.stopPropagation().

Secondly, the controller component tries to change its own state and data. However, the controller does not always need to do this, and it does not always have its own data. Therefore, it is not required to implement these methods.

Thirdly, if the state or data of the controller component has been changed, it calls the method _handleSomethingChangedWhenOperated to update its own display and trigger its own event. But, as mentioned earlier, the controller is not required to modify its data or state.

Finally, the controller calls the method _handleOperated, where it performs additional actions that are not dependent on its state or data but solely on the content of the event.

An example of a controller component that does not change its data or display upon receiving an event is the button itself. It serves as a wrapper for a native component, which triggers the click event that the button responds to. However, the button defines its display and data once and does not change them. Therefore, the button extends the _updateStateCodeWhenOperated method, pretending that its state has changed. This allows the button to trigger the _dispatchEventsWhenChangedAfterOperated method every time it responds to a click event.

Let’s consider an important aspect of event handling implementation. To do this, let’s go back to the very beginning of the process. It starts with blocking event propagation.

It is often the case that control buttons are placed near each other on the same panel, but their processing is handled by different components. For example, imagine a table where each row represents a list of cities. In each row, there are two buttons: one for changing the city name and one for removing the city from the list. The row component handles the name change, while the table component handles the removal. If the row component blocks the events, the table will not be able to intercept them, and the removal process will not occur.

To solve this issue, event codes should be used. It is usually assumed that the event code is needed to distinguish one type of event from another, for instance, distinguishing button click events from input field change events. However, this is not the case. The event code is necessary to classify events within the context of the domain. For example, it differentiates events triggered by clicking the "edit" button from those triggered by clicking the "delete" button. Both are click events, but within the domain, they are different and correspond to different handlers.

In the case of the table rows example, if the events triggered by different buttons have different codes, the rows will subscribe to events with one code, blocking their propagation, while allowing events with a different code to propagate to the table level.

Usage

In many frameworks, the button is used to directly trigger functions. That is, the logic is embedded in the button's click event handler. In my opinion, this is an incorrect approach. With this method, business logic is fragmented and distributed across buttons, input fields, and other components. The components become tightly coupled to the functions, making debugging and modifying the logic a difficult task.

In the KoiCom library, the button’s task is to transmit and handle very specific data – an identifier of an element and a code representing the action that the user wants to perform on that element. The button does not perform any other actions. Its task is to transform the user’s intent into the triggering of an event that contains only the information about the user's intent.

Therefore, the button is always part of a composite component that receives and processes the event. It is this composite component that contains all the necessary logic, encapsulated within itself.

Thus, it is not the button on the form that should be responsible for sending data and handling input errors; it is the form itself, which contains the input fields and the data submission button. The form, as a composite component, should manage the input fields, notifications, and the button’s state.

The button component is particularly useful when creating multi-page lists of elements, each of which requires certain actions. For example, when creating CRUD admin panels.

It is worth noting two points related to the KoiButtonStencil class, the KoiButtonDataCapable behavior, and the KoiButtonNativeButtonSocketConnectable behavior.

According to the standard, a click event can be generated by various components. In other words, by modifying the KoiButtonNativeButtonSocketConnectable behavior, it is possible to create a subclass of KoiButtonStencil that serves as a wrapper not only for a link but also for any other component that generates a click event.

The second point concerns the transmitted data. The koi-operated event can carry not only a reference to data described by the KoiButtonDataCapable behavior but can also carry data of a different type, containing not just the command data, but also any other data.

Moreover, it is possible to implement a flow for KoiButtonStencil that does not have its own data but transmits data from other components in the event, for example, data from a provider related to the button.