Show Sidebar Menu

KoiCom Handling Data and State Management

This article explains how components in the KoiCom library handle data, from initialization to transformation and modification. It covers the component lifecycle, data validation, event handling, and interaction with other components. The focus is on standardizing data handling workflows for consistency and reliability.

In a broad sense, the task of a component is to transform data. From the moment it is created, the component executes several methods to transform the data it receives from tag attributes and possibly from external data sources into its own data. The state object represents the current state of the component during this transformation, while the data object holds the actual data.

Here’s a breakdown of what happens during the initialization of a fairly universal component.

_onConstructed initializing
A state object is created with the value 'initializing'. According to the data schema, a data object is created, with its elements initially set to null, and default values are taken from the data schema. A connector is created, and an attempt is made to connect it to the data provider.
_onBeforeConnected initializing
  1. If any data element has a matching attribute, the default value in the data object is replaced by the attribute value, considering the data type.
  2. The component renders its template in the DOM using a socket and collects references to its internal components.
_onConnected ready
  1. The component updates its state based on its own data, the state, and data from the provider. By default, it assumes the 'ready' state.
  2. The component performs preparatory calculations on the data object using default values and provider data.
_onAfterConnected ready
  1. If the component's state has changed, or if any data element has the _changed flag set to true, the component’s display is updated, and a koi-changed event is fired, after which the _changed flags are reset.
  2. The component subscribes to events from the provider, external components, and its internal components.

The data transformation process outlined here has its own nuances that should be considered when developing components.

Note that the component attempts to update its display and trigger the koi-changed event when it is not yet fully initialized, meaning it hasn't yet subscribed to the events of the provider and other components.

This shouldn't cause any issues, since by the time the koi-changed event is triggered, the component has already prepared its own data, validated it, and can share it with the project and other components.

However, this could lead to two consecutive triggers of the koi-changed event if the provider isn't ready to provide data at the time the component is created. Let's imagine that immediately after the _onAfterConnected phase, the provider performs the necessary actions and transitions into a state where it can provide data. In this case, the component will trigger the koi-changed event a second time in response to the provider.

This is not necessarily incorrect behavior, but at that point, the component might "flicker," displaying the 'loading' state in the interface and then immediately switching to 'ready.'

Also, note that the JavaScript interpreter generally completes the execution of the method that triggers the event before the event handler in another component is called. Therefore, by the time the external handler for koi-changed is triggered, the _changed flags may have already been reset, and you won't see in the handler which specific data elements have been changed.

In principle, you shouldn't rely on the flag values of data elements from one component in event handlers of another component. The purpose of the data element flags is to help the component manage the process of changing its own data. They are not meant to provide information about the state of data elements to other components. The key concept here is "providing information."

The data object is responsible for the overall set of data. It can report that its elements have been changed, but to specify which exact elements were changed, it must expose the details of its implementation. This task is handled by modifying the event object. If you need one component to know which specific data elements of another component were changed during the event handling process, the second component must gather this information and pass it to the first component within the event object body, alongside the state and data objects being transmitted.

Data Modification

By default, a component does not provide public methods for modifying its data, but it can if needed.

If you plan to implement a component whose data can be changed through the invocation of a public method, carefully review the Workflow section again, especially event handling.

Let me briefly summarize the key point. The process of handling data modification should follow a standardized procedure. This is necessary so that when data is modified, the component performs a series of additional actions related to modifying the data according to the domain, updating its state based on the data, and triggering the koi-changed event. For example, the KoiLabel component, within the behavior of KoiStringDataCapable, provides a modification method that looks like this:

export const KoiStringDataCapable = Sup => class extends KoiDataCapable(Sup) {
	...
	attemptChangeValue(new_value){
		this._log('attemptChangeValue() - ' + new_value);
		this.data.setValue(new_value);
		this._updateSomethingWhenChanged();
		this._onAfterChanged();
	}
	...
}

The structure of this method mirrors that of event-handling methods. First, the method that changes the data is called (in this case, data.setValue), followed by the _updateSomething* method, and then the _onAfter* method. By standardizing this sequence of calls, the component responds consistently to both direct method calls for data modification and events, executing the same sequence of actions.

In this standardized process, the most critical method is data.setValue, which is called first. It directly modifies the component's data by interacting with the data object.

export const KoiDataValueChangeable = Sup => class extends Sup {
	...
	setValue(new_value){
		this._properties['value'].setValue(new_value);
	}
}
export class KoiStringData extends KoiDataValueChangeable(KoiData) {
	...
}
export const KoiStringDataCapable = Sup => class extends KoiDataCapable(Sup) {
	...
	_constructData(){
		return new KoiStringData();
	}
}

The setValue method is provided by the KoiDataValueChangeable behavior. This is a very convenient behavior for creating data objects. It encapsulates the interaction with data elements within the data object, allowing the data object to be perceived as a whole, with methods for setting and retrieving values.

Now let's delve deeper and examine the process of modifying a value in the KoiDataElement class:

  1. If the new values are the same as the current ones, no assignment takes place, the _changed flag remains false, and no further actions are performed.
  2. If the new values can be converted to the required format, the _defined flag is set.
  3. The _changed flag is set.
  4. The new value is assigned to the _value property.
  5. Completeness is checked, and additional validations are performed. If necessary, the _valid and _error_code properties are set.

According to the KoiCom library's logic, the component contains the data that was assigned to it, unchanged, even if it is invalid or incomplete. This is important not only for notifying the user about invalid data but also for identifying what exactly is wrong with the data.

For example, when data is modified, the component attempts to cast the data type to the one specified in the data schema.

€ 0

If you execute the following code in the console, you will get false in the first case and true in the second. In the schema of the KoiMoneyLabel component, the value data element has the type integer. All values assigned to this element are cast to this type.

let component_instance = document.getElementById('money_label_1');
let str_a = '100';
component_instance.attemptChangeValue(str_a);
let int_b = component_instance.data._properties['value'].getRawValue();
console.log(str_a === int_b);
let int_c = 200;
component_instance.attemptChangeValue(int_c);
let int_d = component_instance.data._properties['value'].getRawValue();
console.log(int_c === int_d);

If the data conversion fails, the value that was passed to the component is retained in the component's data without modification. If you execute the following code in the console, you will see that the component displayed an error, and the string that was provided is returned in the console.

let component_instance = document.getElementById('money_label_1');
component_instance.attemptChangeValue('abc');
let int_e = component_instance.data._properties['value'].getRawValue();
console.log(int_e);

However, this time, the error property of the data element has changed, which can be accessed by running the following code:

console.log(component_instance.data._properties['value'].getErrorCode());

Naturally, this code should not be called from outside the component, or even from outside the data object, as indicated by the underscore in the name of the _properties property.

Additional Data Validation

As seen from the previous section, the component stores any value passed to its data but includes several properties and flags to assess that value.

There are often cases where built-in validation is insufficient. After all, in specific applications, you are working within a particular domain, so the data object evaluates values according to that domain. In such cases, additional validation is required.

In these instances, the data object does not simply hold values of type string or number; instead, it contains values of other types, such as a conversion rate, a list of phone numbers, or a counterparty's email address. Additional constraints are imposed on the values of these types.

Let's take the KoiDataElementInteger type as an example of how to implement additional validation.

First, a new data type should be created, inherited from KoiDataElement.

class KoiDataElementNumeric extends KoiDataElement {
	...
}
export class KoiDataElementInteger extends KoiDataElementNumeric {
	...
}

Next, implement the getErrorCodeBasedOnState method in the class.

class KoiDataElementNumeric extends KoiDataElement {
	...
	getErrorCodeBasedOnState(){
		let error_code = super.getErrorCodeBasedOnState();
		if(error_code){
			return error_code;
		}
		if((this._min !== null) && (this._min > this.getRawValue())){
			return 'validation_out_of_borders';
		}
		if((this._max !== null) && (this._max < this.getRawValue())){
			return 'validation_out_of_borders';
		}
		return '';
	}
	...
}
export class KoiDataElementInteger extends KoiDataElementNumeric {
	...
	getErrorCodeBasedOnState(){
		let error_code = super.getErrorCodeBasedOnState();
		if(error_code){
			return error_code;
		}
		let value = parseInt(this.getRawValue());
		if(isNaN(value)){
			return 'value_has_to_be_integer';
		}
		return '';
	}
	...
}

Then, use this new data element class in the data schema, and that will suffice.

Retrieving Data

The basic components of the KoiCom library do not have public methods for retrieving data because the components themselves or connectors should be responsible for fetching the data. Both the components and connectors can access the methods of the data object.

This is why, in the examples found in the documentation, I often refer to protected methods and frequently use sequential calls, as demonstrated here:

console.log(component_instance.data._properties.value.getErrorCode());

However, this approach should not be adopted in your projects.

When implementing subclasses of KoiData, which represent the component's own data objects, it is necessary to retrieve data to pass to connectors of other components and to the component's own socket for rendering.

To retrieve data for this purpose, use the getRawValue and getValueOrDefaultValue methods of the data element. If needed, additional methods can be implemented based on the getValueAsHTML and getValueOrDefaultValueTypified methods of the data elements.

The getRawValue method returns the value of the _value property of the data element, that is, the value assigned to the element without regard to the default value.

The getValueOrDefaultValue method returns either the value of the data element or the default value when the element's value is not set.

In most cases, these two methods are sufficient for data transmission. However, in certain cases, methods that simplify data output to HTML or generate JSON may be necessary.

The getValueAsHTML method is used for outputting values into input fields and HTML tag attributes. There is a mismatch between JavaScript values and HTML tag attribute values. In JavaScript, a variable’s value can be undefined, meaning it can be null. The results of parseInt and parseFloat may result in NaN. In HTML tags, all attribute values are of type string. It is undesirable when null and NaN values are converted into the strings 'null' and 'NaN' when rendered in the interface.

Let's imagine that the value null was passed to the component's tag. This would result in the following tag:

<docs-sample-label id="sample_label_2" value="null"></docs-sample-label>

Naturally, it would appear like this:

null

Confusing data output for the user can be easily avoided by using the getValueAsHTML method instead of getValueOrDefaultValue. The getValueAsHTML method converts all values that may appear strange to the user into an empty string.

export class DocsMoneyDataExtended extends KoiData {
	...
	getDataExtended(){
		return this._properties['value'].getValueAsHTML();
	}
	...
}

Additionally, you can create your own method based on getValueAsHTML to convert the data into a different format. For example, for a component that displays a monetary value, you could convert null and NaN into a red question mark or a more user-friendly word like "error".

The getValueOrDefaultValueTypified method is useful for transmitting component data via an API. For REST APIs, GET requests are used. The values transmitted via a GET request must be either strings or null.

Non-null values are converted into strings, and it would look something like this: 'https://domain.com/?var1=123&var2=&var3=abc'. Boolean values are converted into the strings 'true' and 'false'.

A problem arises because when receiving a request, the server might interpret the value 'false' as a string, and a non-empty string is often converted to true when converted to a boolean type.

The getValueOrDefaultValueTypified method converts the false value used for empty input into null, which is correctly recognized when forming a GET request.

Of course, you can avoid sending a request when there are mandatory fields that are not filled in, but there are various scenarios, and for special cases, the library provides the getValueOrDefaultValueTypified method.

Preprocessing and Data Transformation

We previously discussed data preprocessing in the Workflow section. Let's take a closer look at this topic now.

A component stores data in the form it is received from external sources. This is because the component must display notifications regarding the validity of the data. At the same time, the component does not always need to display and transmit the data in the exact format it received them.

The core function of any component is to transform the data. To transform the received data into its own data, you need to define an appropriate method in a subclass of KoiData.

export class KoiFormattedPhoneLabelData extends KoiData {
	...
	_formatPhoneNumber(new_value){
		...
	}
	...
	setValue(new_value){
		this._properties['value'].setValue(
			this._formatPhoneNumber(new_value)
		);
	}
	...
}
export const KoiFormattedPhoneLabelDataCapable = Sup => class extends KoiDataCapable(Sup) {
	...
	attemptChangeValue(new_value){
		this._log('attemptChangeValue() - ' + new_value);
		this.data.setValue(new_value);
		this._updateSomethingWhenChanged();
		this._onAfterChanged();
	}
	...
	_constructData(){
		return new KoiFormattedPhoneLabelData();
	}
	...
}

However, this is generally not required.

If the component's task is to display transformed data, the data transformation should be carried out directly during the display process.

If the component's task is to transform the data and send the transformed value to the server via an API, the data transformation should take place just before sending.