Show Sidebar Menu

KoiCom Web Components Data Flow Through Providers and Connectors

This article explains how components in the KoiCom library interact through data providers and connectors. It covers how providers supply data, how components connect to them, and how connectors serve as adapters. The article also discusses component initialization, error handling, and managing provider data changes within the framework.

In the KoiCom library, some components act as data sources for other components. There is even a special class of components that have no visual representation and whose sole function is to provide data to other components.

A data source in the library is referred to as a provider. Like any component, a provider has its own workflow, during which it is initialized, receives data, stores it as its own data, and responds to changes in its own data. Therefore, the provider is not always ready to provide its data to other components. As a result, the provider shares not only its own data (the data object) but also its state (_state object), and communication between the provider and the data recipient happens through events.

For a component to receive data from a provider, it must implement the KoiSingleConnectorInteractable behavior (or KoiCompositeConnectorInteractable). This behavior creates an internal _connector object in the data recipient, which holds a reference to the provider component. We will refer to this object as the connector.

To connect a component to a provider, you need to assign it the KoiSingleConnectorInteractable behavior and specify the source tag identifier in the provider_id attribute of the component.

In the _onConstructed event handler, the component creates an object of the KoiSingleConnector class. This object automatically creates a reference to the provider component using the provider_id attribute value. Any subsequent modification of the provider_id tag attribute will not change the reference to the provider component. We discussed this in the Attributes section.

Provider as Part of the Connector Composition

Immediately after the creation of the KoiSingleConnector instance, the KoiConnectorInitializable behavior calls the connector's prepare method. As a result, the reference to the provider component is set as the value of the connector's _item property, allowing the connector to access the provider's public properties and methods as if they were its own.

This can be understood as a form of composition based on the "has-a" principle. The provider object is seen as a part of the connector, and the _item property functions as a namespace. The connector can access the provider’s methods just like its own.

One might object: the code does not explicitly state that the connector uses the methods of a specific provider. So, how does the connector know which methods it can use?

That’s true. The connector receives the provider’s identifier at runtime, and the connector’s code does not specify which provider will ultimately be connected. However, I believe there is an implicit indication. To avoid errors and ensure clarity about which provider with which methods is connecting to the connector, it is enough that the connector’s name includes the name of the class used by the provider to create the data object. In our example, the name KoiStringDataConnector clearly implies that it connects to components with the KoiStringDataCapable behavior. JavaScript is largely based on conventions.

Now let’s consider the data transfer chain. The data recipient calls the connector and knows its methods. The connector calls the provider and knows the provider’s methods. Thus, the connector implements the "adapter" pattern.

As an adapter, the connector facilitates interaction between the component and the provider, and it does not necessarily need to have the same methods as the provider. For example, the code shown above could be written as follows:

The connector is one of the possible adapters to the provider, and the provider does not need to be aware that it is being used in a particular context. However, situations may arise where a single provider has multiple similar connectors, and in such cases, I recommend moving the connectors to a separate file and using a Stencil class, organizing the connectors with inheritance and mixins.

Similarly, the connector should not know about the components that will use it. It provides a set of methods (a client interface) but is unaware of its clients (the components that use the connector). It is the clients that need to know which methods the connector offers.

Client Interface of the Connector

The connector can always interact with the provider to retrieve its data and state. To achieve this, the basic connector classes provide a set of ready-made methods. For example, you can check if the provider’s data is ready for use.

It’s also possible to obtain the data object directly. For example, like this:

However, I do not recommend doing this. If the component gets the data object directly from the connector and uses the data object’s methods without going through the connector’s method calls, it could cause confusion when you want to switch from one connector to another within the component.

The second reason I do not recommend this is that the component cannot be certain about the provider's state. The provider could change its state at any time, lose data, or receive invalid data, among other possibilities. While these issues can be accounted for in the connector’s methods, the component should not have to perform such checks every time it accesses the data.

Methods of the Component for Working with the Connector

By using the connector, components no longer need to know the implementation details of the provider. However, components must be aware of and use the connector's interface. To ensure clients are familiar with the connector's interface, we will use behaviors.

Now, the KoiConnectedComponent can use its own _getValue method to access the provider's data without being aware of the provider itself and without worrying about the connector's interface. The connector's interface is encapsulated within the KoiStringDataConnectorInteractable behavior.

Using behaviors is recommended but not mandatory. It’s also possible to access the connector directly from the component.

However, in my view, using behaviors makes the code cleaner. Furthermore, based on development experience, isolated behaviors might later prove useful when implementing other components.

Creating the Connector Object

The KoiConnectorInteractable behavior depends on the component implementing the _constructConnector method in some way, which returns the connector object.

Typically, I move the implementation of this method into a behavior.

I don’t see much of a difference between these two options. Especially because, when making small modifications to the connector that don't alter its interface, there's no point in creating a new behavior. However, it’s necessary to point the component to the new connector. Therefore, you will encounter both approaches for creating the connector object in the KoiCom library.

Component Dependency on the Provider

In the KoiCom library, the provider is viewed exclusively as a data source. To manage external and internal components, sockets should be used. However, even as a data source, the provider has a significant impact on the component's behavior.

Once the component activates the connector, it becomes dependent on the provider’s state. Any changes in the provider’s state or data trigger the koi-changed event, which is handled by the component in the _attemptApplyConnectorDataChanged method.

The reason is that the provider can change its state at any time, for example, losing connection with the server or receiving invalid data. The component must react appropriately. It needs to understand why the provider cannot provide data and display an appropriate notification to the user using the specified display methods.

Developers often neglect error notifications or display vague messages. This should be avoided. It complicates things for users, support teams, and ultimately, for the developers themselves. Good luck figuring out why the message “Task failed successfully” appeared.

Here’s another analogy for what a component is. If a component’s task is to transform data, then the component itself is like a measuring tool that connects to a system, allowing the user to see the system's status and possibly control it. This analogy aligns well with the goal of the KoiCom library: to provide a user interface for interacting with a headless API.

So, the tool connected to a broken system should show the reason for the failure. By implementing the KoiConnectorInteractable behavior, the component automatically receives the provider's state, and by implementing the _updateOwnDataWhenConnectorDataChanged method, the component must check if the provider can provide data. To do this, the component can use the connector’s canProvideData method and other methods to identify the cause of the error.

There’s also a difference in how the connector and the component perceive data. For the connector, the data might be perfectly fine, while for the component, it might not be suitable. In this case, a method extending canProvideData can be added.

Although I’ve encountered such issues very rarely in practice, I recommend that when developing components, you keep in mind that data in the connector, especially if obtained externally, may not be suitable for use.

Component and Provider Initialization Order

The component and the provider can be created and rendered in the DOM at different times. As a result, the component might miss events from the provider or may not be ready for them. Let’s consider a few scenarios for the order in which the component and its provider might be rendered in the DOM.

Scenario 1: The provider is rendered in the DOM first, followed by the component. In this case, the provider first prepares its data and triggers the koi-changed event. After that, the component is rendered in the DOM, prepares its data, and by the time _onConnected is called, both the component’s and the provider’s data are ready for display.

In this case, during the _updateAppearance call in the _onAfterConnected handler, the component is not yet subscribed to the data source's events but can still display the provider's data. Let’s simulate this scenario.

As you can see, everything works properly in this scenario, and the component on the right displays its own data from the provider on the left.

Scenario 2: The component is rendered in the DOM first, followed by the provider. In this case, during its creation, the component will not be able to retrieve data from the provider and will only react to it after the provider triggers the koi-changed event.

Let’s simulate this scenario by initializing the component (customElements.define) before initializing the provider.

As you can see, in both cases, the component and the provider are correctly initialized, and the component receives data from the provider. However, in the second scenario, the relationship between the component and the provider is somewhat more complex. In this case, both the connector and the provider have debug_mode enabled, and you can see the order of function calls in the console.

At the time of _onConnected, the component updates its state based on the provider’s state, which has not yet been initialized. As a result, the component enters the "loading" state. Only when the component's connector receives the koi-changed event from the provider does the component initiate the display process again.

In the example above, the provider was never initialized (which is why it is not visible), and as a result, the component displays its own "loading" state.

Provider Events

Now, let's examine how the component reacts to various changes in the data source. We will start by modifying the data itself.

Try changing the value of the data in the source by running the following code in the console:

As you can see, when the source data changes, the component's data is also updated. Now, let's check how the component responds to a change in the source's state.

In this case, the component used the displayWaiting method to display a notification that it could not load the component's data because it is in the 'loading' state.

Note: Here, we directly used the 'loading' code, but in projects, I recommend using static methods like KoiState.getLoadingCode().