In the previous article, we discussed why data encapsulation is a key characteristic of a well-designed web component. A web component, as a self-contained structure, should minimize external dependencies to ensure ease of use, portability, and testing. However, this encapsulation presents developers with a new challenge: if a component is "isolated," how can it be initialized using external data?
This question opens up a whole range of fascinating challenges related to passing data into web components. Get ready — it’s going to be overly tedious, just the way you like it!
Initialization of Web Components
Initializing a web component is the process of getting a custom element ready to work within a web application. In simple terms, it involves creating an instance of a class registered with the customElements.define method and running the code defined in the component’s lifecycle methods, such as the constructor and connectedCallback.
As we discussed in the previous article, during initialization, the component must establish its local state — essentially, the data object it will be working with. This object can be populated with default values, but more often than not, external data is needed to fill it.
The component must somehow receive this external data, meaning the data has to be stored somewhere from the start. These data are passed to the component at the initialization stage. As a result, the component requires a specific environment that handles the preparation, storage, and transfer of the data, as well as kicks off the initialization process itself.
What Should the Environment Be Like?
The simplest case of initialization is for an autonomous component. An autonomous component is independent of any environment or external factors, making it highly versatile. It can be integrated into any part of a document — whether it's a page with minimal structure or even a completely blank one. This approach significantly simplifies development because you don’t need to account for the specifics of the external environment, and testing becomes much easier. Developers can isolate the component and test it in a clean environment without needing to recreate the context. Not only does this save time, but it also eliminates potential risks that could arise from changes in the environment that might affect the component’s functionality.
However, most components perform more complex tasks, including interacting with other elements or external data sources. For this, they require an environment. In such cases, it’s crucial that the environment maintains as much simplicity as possible. Ultimately, developers aim to combine the advantages of autonomous components with the ability to function within a more complex system. This can be achieved by ensuring that the environment remains as lightweight and user-friendly as possible, approaching the simplicity required for autonomous components.
So, what characteristics should such an environment have? A simple environment is one that can be set up quickly, with minimal effort. For this to be the case, it should be understandable, compact, and familiar to the developer. When a developer faces a task that requires a minimal amount of action and uses widely accepted approaches and standards, the work becomes easier and faster to accomplish.
For example, if you’re programming web components, you’ll immediately understand what the following code does. You’ll be able to either repeat it from memory or simply copy and paste it into your project without wasting much time.
<script>
class SomeComponent extends HTMLElement {
connectedCallback() {
}
}
customElements.define("some-component", SomeComponent);
</script>
<some-component></some-component>
That’s why the key characteristics of a simple environment are the use of standard terminology and widely adopted approaches. The closer your code is to the standards, the easier it will be to understand, use, and deploy.
Simple Placement
Let’s dive deeper into the topic of placing a component within an environment. What exactly do we mean by "placement"? Here, we’re referring to everything related to positioning: this could involve placing the module file of the component, the component's JavaScript code itself, or the HTML tag that adds the component to the page. Regardless of what we’re placing, it’s crucial that the placement rules are clear, understandable, and don’t require complex conditions to be followed.
To understand why this is so important, let’s look at a typical example from standard HTML markup. We know that the li tag should usually be inside a ul tag. But what happens if we place the li inside a div? Or, conversely, if we nest a div inside a ul, and put the li inside the div? Here’s an example of such a structure:
<ul>
<div>
<li></li>
<li></li>
</div>
</ul>
At first glance, this may seem like a small mistake, but this kind of rule violation can lead to unexpected consequences. Why? Because the HTML specification clearly defines rules for the placement of certain elements relative to each other. This creates additional questions and confusion, even with well-known tags.
Now, imagine that we establish strict rules for placing our component within the environment. This could raise even more questions for developers, especially for those who are just starting to work with our component. For example, should the component only be placed in a specific section of the page? Do its neighboring elements need to follow certain conditions? Having strict placement rules can complicate working with the component.
From this, we can draw an important conclusion: the environment will be simpler, and the component more user-friendly, if its use doesn’t depend on strict placement requirements. Ideally, a component should be flexible enough to be placed anywhere on the page without any additional conditions.
Environment Composition
The more complex the composition of an environment, the higher its overall complexity. This is obvious: performing one operation is always easier than performing several. Each additional operation increases the chance of error, whether it's a forgotten action or an incorrectly executed step. Furthermore, the more steps involved in a process, the more time it takes, which affects overall performance.
Let’s look at this in the context of working with components. When a component requires just one attribute to be specified, working with it is simple and intuitive. However, when a component requires setting five attributes at once, the task becomes significantly more difficult. It’s even more complicated if the values of some attributes depend on others. This interdependency increases the likelihood of errors and demands more attention from the developer.
For example, I once worked with a component that required setting an initial value and boundary values. Although the boundary values had default values, I frequently forgot that they might not be suitable for the specific project. This led to errors that had to be fixed by going back to the documentation or rechecking the code. Here’s an example of such a component’s code:
<script>
class SomeComponent extends HTMLElement {
connectedCallback() {
this._maximum_value = this.hasAttribute("maximum_value") ? parseInt(this.getAttribute("maximum_value")) : 10;
this._initial_value = this.hasAttribute("initial_value") ? parseInt(this.getAttribute("initial_value")) : 5;
}
}
customElements.define("some-component", SomeComponent);
</script>
<some-component initial_value="15"></some-component>
Here you can see that the maximum_value attribute has a default value, but it can also be explicitly set. However, in real-world projects, default values don't always meet the current requirements. If this is overlooked, errors can occur that are not immediately obvious.
From this, an important conclusion can be drawn: the fewer parts an environment has, the easier it is to work with. Every new element adds complexity, so minimizing the number of required configurations and dependencies helps make the process more understandable, convenient, and efficient. Design environments in such a way that they require minimal actions from the user to get started, and you will significantly simplify their use.
Accessing the Environment
Let’s consider situations where a component needs to interact with its environment during initialization. To do so, the component must have the ability to access the environment — whether it's variables, objects, or events. However, for such interaction to be successful, the component must "know" its environment, or more precisely, have a clear way to identify it.
A simple example: let’s assume the component needs to retrieve the content of another element. This can be done as follows:
<script>
class SomeComponent extends HTMLElement {
connectedCallback() {
this._text = document.getElementById(this.getAttribute("div_id")).innerHTML;
}
}
customElements.define("some-component", SomeComponent);
</script>
<div id="some_id">Some contents</div>
<some-component id="asd" div_id="some_id"></some-component>
Let’s not dive into the flaws of this code (and there are indeed several). Instead, let's focus on an important point: the component is accessing another element using a specific identifier (div_id). This approach simplifies the task because using a specific reference or name is the most intuitive way to interact with the environment. The component knows exactly where to look for the data, and it doesn’t need to scan the entire environment to find the required element.
The same principle applies to events. If the component needs to intercept an event, it’s easiest for it to do so if it knows the code or name of the specific event it needs to handle. The clearer and more precise the point of interaction, the less likely an error will occur, and the easier the component’s integration process becomes.
The key to simplifying a component’s interaction with the environment is having a variable, constant, or other mechanism that limits the scope of the search. If the component doesn’t need to "know" the entire environment but only needs to work with a specific part of it, the initialization process becomes much easier.
Changing the Environment
Working with names is key to creating flexible and versatile components. When a component accesses something in its environment by name, switching from one environment to another becomes much easier if all that's needed is to replace the name. This approach minimizes the effort and reduces the chances of errors when adapting the component to new conditions.
Let’s consider an example from the previous code. Suppose we want to use two components, each of which accesses its own unique div:
<div id="some_id_1">Some contents 1</div>
<div id="some_id_2">Some contents 2</div>
<some-component div_id="some_id_1"></some-component>
<some-component div_id="some_id_2"></some-component>
As you can see, the implementation here is simple and clear. The components interact with different elements by simply getting their identifiers through the div_id attribute. This makes them universal and independent: by specifying a new name, you can easily reconfigure the component.
Now imagine a situation where the div identifier is "hard-coded" into the component’s code or, even worse, the component refers to a global variable. For example:
<script>
const global_const = 2;
class SomeComponent extends HTMLElement {
connectedCallback() {
this._text = global_const;
}
}
customElements.define("some-component", SomeComponent);
</script>
In this case, the component will always use the value of the global_const variable, regardless of the environment it's in. This creates a rigid dependence on the global state and complicates the adaptation process. If you need to change the behavior of the component, you’ll have to edit the code or modify global variables, which isn’t always convenient or safe.
So, the important conclusion is this: an environment becomes simpler and more convenient if it provides the component with the ability to work with names that are easy to replace.
Data Retrieval
When a component interacts with its environment, the primary responsibility for the correctness of this process lies with the component itself. The component is the one that must use the name to access the necessary data. However, the environment also plays an important role: it must provide the data in a way that makes it easy for the component to use.
Let’s consider an example from the previous code, where the component directly accesses a global variable. In this case, changing the environment name becomes very difficult because the component is tightly coupled to a specific variable. If a different variable is needed, the component code must be rewritten. This is not only inconvenient but also reduces the component's flexibility and reusability.
Now, let’s improve the approach a bit:
<script>
const global_const = 2;
class SomeComponent extends HTMLElement {
connectedCallback() {
this._text = eval(this.getAttribute("const_name"));
}
}
customElements.define("some-component", SomeComponent);
</script>
<some-component const_name="global_const"></some-component>
In this version, the component gets the variable name through the const_name attribute. This provides more flexibility: to use a different variable, it’s enough to pass a new name through the attribute. Of course, using the eval method is not an ideal solution. It carries potential security risks and can decrease performance. However, even this approach demonstrates how the environment change can be simplified by providing the component with a more convenient way to access data.
This leads to another important rule: an environment becomes simpler if it offers the component a convenient and understandable way to access data.
Conclusion
In this article, I’ve tried to cover the key criteria that help assess the simplicity of the environment for initializing a web component. These criteria not only help to understand how easy it is to work with a component but also allow you to find ways to improve the interaction between the component and its environment. However, I’m sure that I haven’t covered all possible aspects. If you have any ideas, thoughts, or examples, I would be happy to consider them and include them in the article.
In the next article, I plan to dive deeper into the topic and discuss specific approaches to data transfer between components. We will analyze them using the criteria of simplicity, convenience, and flexibility outlined here. This will help us choose the most effective and versatile methods suitable for a wide range of tasks and scenarios.
Based on the best practices I identified during my work, I created the KoiCom library.
KoiCom documentation
KoiCom github
It already incorporates the most successful ways to handle the interaction between components and their environment. I sincerely hope that this library will be useful to you and help simplify the development of web components. If you have any questions or feedback regarding its usage, I would be happy to hear from you.
Top comments (0)