DEV Community

Valery Uninum
Valery Uninum

Posted on

Next-Generation Buttons: Implementing the Command Pattern through Web Components

When I start designing an interface, I always encounter the issue that event handlers are directly attached to buttons, which limits the flexibility of component interaction. The problem is that standard buttons cannot offer any other behavior. What I need is logic isolation and dynamic action management, which are not available when using standard buttons 'out of the box.' In this article, I will propose a solution on how to adapt buttons using Web Components and the 'Command' pattern, opening new possibilities for more flexible and scalable interfaces.

The Button is More Than It Seems

Usually, when we think about a button, we perceive it as a graphical control element that provides a simple way to trigger some event, action, or change in the interface’s state. This is a very straightforward and convenient definition, which fits our everyday understanding of user interface elements in web applications.

However, when we encounter a button in the context of web development, when HTML and JavaScript are involved, the first thing that comes to mind is the standard tag, which is the most commonly used tool for creating buttons on web pages. This tag typically looks like this:

<button onclick="myFunction()">Click me</button>
Enter fullscreen mode Exit fullscreen mode

But if we think about it, this tag, while serving as a button, doesn't fully reflect all the possible aspects and functions that a button can perform in the broader context of user interaction with an interface.

Upon closer inspection of the definition of a button, one might notice that it doesn't provide any information about how the button should look, how it should behave, or how it should trigger an action. In this context, there’s no clear understanding of what is meant by the words “a simple way” to trigger an action, nor how the connection between the button and the action is established. We only see the basic structure of a button, which, when clicked, calls some method. But in reality, this simplicity hides a much broader range of possibilities and approaches. And so, the question arises: perhaps the button is more than just the tag we see in the example above?

A Simple Way to Trigger an Action

Let’s approach the concept of a button from a more philosophical perspective, delving into its essence and function. What does a button really represent? If we consider the essence of a jug as its emptiness, the essence of a button can be found in its ability to initiate an action. A button is not just a user interface element; it’s a mechanism that triggers a specific process that already exists within the context of the application. The action to be performed happens within the application, in the context of the entire system, but the initiation of that action — its start — is the button’s function. Therefore, we see that the button serves as a kind of trigger, launching an action in a broader external system context.

When a user clicks on a button, they expect that this click will lead to a specific action. Consequently, the button is attributed with the responsibility for initiating this action. In other words, the button becomes the link between the user and the actions that should follow. However, it’s important to note that the method or function that actually carries out the action should not be aware that the button was the one that triggered it. This distinction between what initiates the action and what performs it is a crucial aspect that allows us to maintain flexibility and ease of interaction in more complex systems.

When the button directly performs the action or when the method implementing the action depends on the button itself, we are dealing with a rather complex and interdependent system. If we wish to simplify such a system, it’s necessary to break it down into simpler, independent parts. And here we conclude that simplifying the process of initiating an action primarily involves separating the initiation process from the action itself. And since in the context of JavaScript, the initiator is often referred to as an event, we are talking specifically about separating the event as the initiator from the logic that executes the action.

Separation of Event and Handler

Why is it important to separate the event from the handler?

First of all, separating events from handlers significantly improves the readability of the code and promotes the creation of more modular solutions. When the button logic and its handler are intertwined, or, even worse, when the handler is an anonymous function, the code becomes extremely difficult to read and analyze. This can lead to problems when maintaining and updating the project, as understanding what the button actually does and what changes are required becomes a challenging task. In contrast, when the handler is extracted into a separate, well-named function that clearly reflects the action being performed, the structure of the code becomes more transparent. The developer immediately understands what happens when the button is clicked, and can more easily modify the behavior of the element without needing to delve into the rest of the logic. Thus, separation simplifies both reading and making changes to the code.

Secondly, separating the event logic and the handler opens up opportunities for reusing handlers across different parts of the application. When the handler is placed in its own function, it can be applied not just to one button, but to many others that have similar behavior. For example, multiple buttons performing the same action can use the same handler, which reduces code duplication and increases efficiency. Furthermore, the handler can be triggered not only via the button but also through other means, such as programmatic calls or actions initiated by other parts of the interface. This significantly expands the functionality of your application, increasing its flexibility and scalability.

Thirdly, separating events and handlers allows for more flexibility in the buttons themselves. If the behavior of the button is now determined not within the button itself, but via a separate handler, it becomes easy to modify its actions or reassign them depending on the situation. This is especially important in projects with dynamic interfaces, where the behavior of elements can change in response to user actions or changes in the application state. This approach allows the interface to be easily adapted to evolving requirements without disrupting the overall code structure.

Fourthly, the separation of events and handlers is crucial for testability, particularly in large projects. When event handlers are extracted into separate functions, testing them becomes much easier, as they can be tested independently of the interface. You can isolate the handler and test how it works with various parameters, without worrying about interaction with other parts of the interface. This makes testing easier, improving the reliability and stability of the application while minimizing the likelihood of errors.

Separating the button event and handler is a key step toward a cleaner, more flexible, and maintainable code architecture. This is especially important in complex projects, where the interactions between interface elements become more intricate and interdependent. This approach helps improve system stability, makes it easier to expand and modify the application, and reduces the risk of errors arising during these changes.

An example of separating a button’s event from its handler can be found in any beginner’s guide.

<button id="myButton">Click me</button>

<script>
  function handleClick() {
    alert('Button clicked!');
  }
  const button = document.getElementById('myButton');
  button.addEventListener('click', handleClick);
</script>
Enter fullscreen mode Exit fullscreen mode

Event Delegation

There is a more elegant and efficient approach to handling events that greatly simplifies managing them in complex interfaces: event delegation. Instead of attaching an event listener to each button or element individually, you can intercept the event on a parent element or even at the document level. This is made possible by the bubbling property (bubbles: true), which allows an event to "bubble up" through the DOM tree from the target element to the root. This method offers several key advantages and can significantly streamline your code, making it more scalable and efficient.

Event delegation is a technique where an event listener is assigned not to each individual element but to a common parent element that encompasses multiple child elements. When an event occurs on one of the child elements, it propagates to the parent, allowing the handler at the parent level to determine the source of the event (e.g., the target element via event.target) and execute the appropriate action. This approach reduces the number of event listeners in your code and enables centralized management of events for multiple elements.

Event delegation is particularly useful in dynamic interfaces where elements like buttons or other controls are added to or removed from the DOM during the application’s lifecycle. In such cases, delegation enables you to handle events for elements added to the interface after the page has loaded, without needing to re-attach event listeners to new elements.

Delegation is also ideal when dealing with a large number of similar elements in the DOM that require the same event handling logic. For example, if a page contains many buttons that should trigger the same action, delegation avoids attaching individual listeners to each button, resulting in cleaner and more optimized code. Furthermore, by reducing the number of event subscriptions, delegation lowers overhead, contributing to better performance.

Like any technique, event delegation has its limitations. Firstly, it only works for events that support bubbling (such as click). Complex events that don’t bubble or need to be handled directly on the element may not be suitable for delegation. Secondly, in scenarios where handlers need to be tightly bound to specific elements — such as when unique parameters or contexts are required — delegation can become cumbersome and may not always be the best solution.

For buttons, their events inherently support bubbling, so this limitation can be safely disregarded. However, the aspect of uniqueness, where specific actions are tied to particular elements, requires careful consideration and will be explored further later.

In summary, event delegation is a powerful and flexible tool for managing events in complex interfaces. It enhances performance, simplifies code maintenance, and is especially valuable in scenarios where the DOM structure is dynamic or when dealing with numerous similar elements. If you encounter such challenges, event delegation is likely the optimal choice for improving efficiency and reducing code complexity.

Here’s a simple example of event delegation. Imagine you have a list with several items, and you want to handle clicks on them using a single event listener:

<div id="list">
  <button>First Button</button>
  <button>Second Button</button>
</div>

<script>
  function handleClick() {
    alert('Button clicked!');
  }
  const list = document.getElementById('list');
  list.addEventListener('click', handleClick);
</script>
Enter fullscreen mode Exit fullscreen mode

The Bubbling Behavior of the click Event

Have you ever wondered why the click event for buttons is designed to bubble up the DOM by default? At first glance, this might seem like a small detail, but it’s a thoughtful decision that has significantly simplified working with buttons in user interfaces. The reasoning lies in how buttons are used in real-world projects.

Buttons are unique interface elements, and their characteristics make event bubbling especially important for them. Buttons play a key role in user interactions, triggering actions in various contexts. Event bubbling ensures flexibility, convenience, and efficiency when working with buttons, addressing several specific challenges.

First, buttons are ubiquitous and frequently used. They appear in almost every interface, serving functions ranging from submitting forms to managing complex systems. Since buttons are often repeated and exist in large numbers, handling their events at the parent element level eliminates the need to create individual handlers for each button, simplifying code development and maintenance.

Second, buttons are frequently created and removed dynamically. For instance, in task list interfaces or product card layouts, new buttons are added to the DOM based on user actions. Event bubbling allows developers to handle clicks on such buttons even when they are dynamically added or removed. This is particularly convenient because it eliminates the need for additional steps to attach handlers to newly created elements.

Another important aspect of buttons is their dependency on context. For example, a button within a modal window might perform actions relevant only to that specific modal. Event bubbling makes it easy to consider context: you can set up a handler at the parent level or even at the document level while retaining the ability to determine which button was clicked and where it is located.

Moreover, bubbling optimizes interface performance. If every button required its own event handler, this would significantly increase the number of event subscriptions, potentially reducing the responsiveness of the application — especially in interfaces with a large number of buttons. Bubbling enables developers to use a minimal number of handlers to manage events for a multitude of buttons, making the interface more efficient.

Finally, buttons often perform similar actions. For example, "Add to Cart," "Delete Item," or "Open Menu" buttons typically share similar logic. Thanks to bubbling, developers can create universal handlers that work with any number of buttons without being tied to specific instances.

In conclusion, buttons, as interface elements, possess unique characteristics that make event bubbling not just a convenient feature but an essential one. By leveraging bubbling, developers can create cleaner, more efficient, and more adaptable code, ensuring seamless interaction with buttons in both static and dynamic contexts.

Event Payload

We’ve established that button events should follow the bubbling pattern. Now, let’s take a closer look at what an event actually represents.

In the DOM, an event object provides the context of an interaction, answering the question of how a specific action occurred. It describes the physical interaction between the user and the interface — for instance, a click on an element, text input, or a key press. However, it doesn’t address the more significant question: why the user initiated this interaction in the first place. Here lies a key contradiction: a user presses a button with a goal or intent in mind, while the event merely records the fact that a click occurred.

When a user interacts with a button, they expect a specific action to be performed, such as "submit a form," "delete an item," or "save changes." To them, the button is a way to trigger a command. However, the click event triggered by the button only provides basic contextual information — such as which element was clicked, where it is located in the DOM, and potentially some custom data via CustomEvent. It conveys nothing about the user’s intent, leaving the interpretation of that intent entirely up to the handler.

This gap between user intent and the technical representation of an event introduces additional complexity for developers. Developers must manually “bridge the gap,” connecting events to application logic using attributes like data-* or the detail property. This approach adds unnecessary coupling and complicates code maintenance, particularly in scalable interfaces with dynamic updates.

For example, consider the following code snippet, which demonstrates an awkward implementation. Here, we have a button that, when clicked, is supposed to pass information about a selected item — such as its ID and name — via the event.

function handleButtonClick(event) {
  const customEvent = new CustomEvent("buttonClicked", {
    detail: {
      elementId: "123",
      elementName: "Example Item"
    }
  });
  event.target.dispatchEvent(customEvent);
}

function handleCustomButtonClick(event) {
  const { elementId, elementName } = event.detail;
  console.log(`ID: ${elementId}, Name: ${elementName}`);
}

const button = document.getElementById("myButton");
button.addEventListener("click", handleButtonClick);
button.addEventListener("buttonClicked", handleCustomButtonClick);
Enter fullscreen mode Exit fullscreen mode

If buttons could convey not only the context of an interaction but also the user's intent explicitly within the event, it would significantly simplify the architecture. Handlers could focus on executing tasks rather than assigning logic to events.

This highlights the need to move away from the traditional understanding of a button as a mere event initiator. Instead, it suggests adopting a more advanced model where the button acts as a bridge between user intent and application logic.

Using the Command Pattern

To create a more advanced model for event handling, we can leverage the Command pattern, which allows events to be linked with application logic at a higher level of abstraction. This can be achieved by introducing a layer that transforms ordinary events into commands such as saveDocument or deleteItem. Using this approach, an event becomes more than just a signal that something has occurred — it transforms into what it is meant to be: the initiator of an action, as discussed earlier in the article.

But this raises a question: why didn’t the developers of JavaScript events implement the Command pattern from the start? Why were events designed as they are now? And why were events necessary in the first place?

When HTML and related technologies like the DOM and JavaScript were initially developed, their primary goal was to create a simple structure for hypertext documents that would allow users to interact with web pages. At that time, user interaction was significantly limited, and the event-handling model was not designed to accommodate complex mechanisms such as the Command pattern. It’s essential to understand that the early web was developed to simplify the creation and management of content, not to provide sophisticated tools for complex client-side logic.

In the 1990s, when HTML and the web were being created, their focus was on providing a straightforward way to present hypertext documents with minimal user interaction. The main goal was data submission to servers rather than executing complex logic within the browser. Buttons and forms were primarily used to send data, not to initiate client-side processes. All computation and data processing were handled on the server, with buttons serving as interface elements that triggered data submission to the backend.

The Command pattern requires a more sophisticated structure that involves clear separation between the interface and processing logic, as well as a mechanism to specify the exact action to be executed. These ideas only became relevant later, as the need for dynamic interfaces and greater interactivity in web applications grew. Dynamic and complex interactions, such as triggering client-side logic through events, necessitated new approaches, including the adoption of the Command pattern.

Can the Command Pattern Be Applied to Buttons Today? Yes, it can. While standard HTML buttons don’t directly support the Command pattern, modern technologies like custom events allow us to create similar mechanisms. For example, we’ve already explored how the detail property can be used to pass additional data with events.

However, this approach is still not ideal, as it requires creating separate implementations for each button in the interface. This adds extra complexity and makes scaling such systems more challenging.

Web Components

Leveraging Web Components to modernize buttons and align them with the Command pattern is a promising approach that can significantly enhance both the architecture and the flexibility of interactions in your project. Web Components provide powerful tools for creating reusable interface elements that can be seamlessly integrated into various parts of an application.

Instead of writing separate handlers for each button, you can create a unified component that acts as a button with the added ability to pass a command. This approach not only improves the structure of the code but also enhances its readability and maintainability.

Here’s a basic example of such a component:

<script>
class CommandButton extends HTMLElement {
  constructor() {
    super();
    this._event = new CustomEvent('commandExecuted', {
      detail: {
        command: this.getAttribute(command)
      },
      bubbles: true,
      composed: true
    });
  }
  connectedCallback() {
    this.innerHTML = `<button>Click me</button>`;
    this.querySelector('button').addEventListener(
      'click', 
      this._onClick.bind(this)
    );
  }
  _onClick() {
     this.dispatchEvent(this._event);
  }
}
customElements.define('command-button', CommandButton);
</script>

<command-button command="saveDocument" label="Save"></command-button>
Enter fullscreen mode Exit fullscreen mode

Button Component and Controller

When a button component transmits a command identifier and potentially additional parameters, it establishes the foundation for a more advanced architecture. In this setup, the component containing the button and subscribing to its events essentially acts as a controller that processes the command passed through the event.

In architectural patterns such as MVC (Model-View-Controller), the controller serves as an intermediary between the model, which represents the data, and the view, which constitutes the user interface. It receives user input, such as button clicks, and manages the resulting changes to the data or state, which are then reflected in the interface.

The use of a controller within a component offers several key advantages. First, it encapsulates the logic for executing commands, keeping the main application code free from unnecessary complexity. The details of implementation remain hidden within the controller itself. Second, this approach enhances modularity, allowing buttons to be reused simply by passing different commands and parameters. It also reduces the coupling within the application, as changes to the command-handling logic require modifications only within the controller, without affecting other parts of the system. Finally, controllers provide significant flexibility. They can handle both straightforward commands, such as "save" or "delete," and more complex actions, while the button component remains simple and focused solely on its primary role.

This architecture facilitates a clean separation of concerns. The button component emits a custom event that includes the command and its relevant data, while the parent component, acting as the controller, listens for this event. The controller processes the command, interacts with the data model if necessary, and updates the user interface accordingly. This approach results in a cleaner, more scalable architecture that is easier to extend and maintain, while keeping the button components reusable and independent of the logic they trigger.

Conclusion

In conclusion, the approach where a button not only triggers an action but also transmits a command with the necessary data through an event is an excellent example of applying the "Command" pattern. This method significantly improves interface interaction organization by separating the logic of command execution from the interface elements, enhancing the flexibility and scalability of applications.

However, such an approach is still relatively uncommon in practice. Instead of leveraging the powerful capabilities of Web Components to create universal and flexible solutions, many developers continue to rely on standard buttons directly tied to event handlers. This is likely due to habit and a lack of awareness about the advantages of this approach, leading to the more conventional use of buttons as simple triggers for actions.

Determined to change this situation, I developed the KoiCom library, where many components have already been adapted and enhanced. In particular, buttons in this library follow the "Command" pattern, transmitting the necessary data and commands via events. This approach greatly increases modularity, flexibility, and maintainability, eliminating redundant logic and simplifying how commands are managed.

KoiCom documentation
KoiCom github

Ultimately, I hope such solutions will help developers adopt a more modern approach to interface design, making applications more scalable and easier to maintain.

Top comments (0)