Now that we’ve covered the theory, it’s time to put it into practice by creating a composite component with an external data source — a standard CRUD panel.
Structure
We’ll divide the panel into its most independent parts. To do this, we’ll set aside its visual representation for now and focus on the processes involved during the panel’s operation and the data being processed.
When the page loads, the panel automatically retrieves a list of records. Each record can be modified or deleted. The entire list can also be reloaded, and new records can be added to it.
Loading the list — whether it’s an initial load or a reload — resets the panel to an initial state, with the loaded data serving as the starting dataset for subsequent operations. This means the panel operates with an initial and a final state. If we use a provider to fetch data, its role is limited to obtaining data from the server and triggering an event to reset the panel to its initial state. The provider itself should not change its data based on further actions.
Further actions include modifying and deleting records. These actions affect only the specific record involved. Users can interact with multiple records simultaneously, sending a request to modify one record while the server processes a prior request for another. Once the server response is received, it should update the relevant record’s display without affecting others. For this reason, modifying and deleting records require a dedicated provider for each record, ensuring updates are isolated to the relevant record.
Adding a record is similarly specific to a single record. After sending a request to create a new record and receiving its data, the panel should display the new record without altering any other records. This task should be handled by a separate provider designed explicitly for record creation.
The panel’s final state — the cumulative set of records — can be constructed by processing all events from all providers. The panel itself can implement behavior inherited from KoiDataCapable to store the resulting dataset within its own state.
Based on this breakdown, we’ll need to implement three types of providers, operating on two types of data. One provider will manage the data representing the list of records, while the other two will handle data related to individual records.
Project
As mentioned earlier, I prefer to keep files reasonably organized. You never know when the current project might evolve into a reusable library for future projects. For this reason, I’ll follow the file structure outlined in the "Structure" chapter.
To start, we’ll create a crud directory inside the js directory. Within crud, we’ll add two subdirectories: controls and providers. Let’s begin with the providers directory. Inside it, we’ll create a file named crud_sample_provider.js, which will act as our provider. This provider will facilitate interaction with server-side data — or, more precisely, simulate such interaction.
Data Object for the List
We’ll start building the provider by focusing on the data structure. Let’s assume our CRUD panel is designed to manage a list of names. While a simple array could represent this list, working with a database typically requires modifying a specific record using its unique identifier. As a result, our data structure needs to be more robust than a standard array.
We’ll create a subclass of KoiDataElement to manage and interact with this data structure. For clarity, we’ll also add two static methods: getHeader and getSampleData. These methods will make the structure easier to understand and will also prove useful in subsequent development.
You can add, modify, and mark records as deleted within the structure using the deleted flag. While outright deletion is an option, there might come a time when the ability to restore deleted records is necessary. Hence, using a flag is a more flexible approach.
Modifying records involves updating the value of the data element. Therefore, any method that modifies records must also update the data element's flags by using _setDefined and setChanged methods, followed by validation with the validateValueAndMarkValidity method.
Next, we’ll implement a data object for the provider. This object will serve as an interface for interacting with the data element.
Primary Provider
Let’s create a provider responsible for storing the list’s data object and supplying initial data to other components.
We’ll set the provider to start in the loading state and attempt to load data immediately upon connection.
To ensure the provider transitions to the loading state upon connection, several strategies are possible. However, on closer consideration, the provider should remain in the loading state whenever it has no data to share with other components — not just initially. Therefore, we’ll adjust the _getOwnStateCodeBasedOnOwnData method to include a check for the presence of data.
The _loadDataWhenConnected method addresses the provider’s behavior concerning data retrieval. This behavior outlines how the provider communicates with the server, retrieves data, and converts it into its internal format. At this stage, we will simulate data loading to avoid dealing with potential data retrieval or API errors. Since this is a simulation, we’ll name the behavior CRUDFakeDataLoadable.
The CRUDFakeDataLoadable behavior initiates the _startDataLoading method during component initialization and ensures that placeholder data is transformed and stored as the component’s internal data. This behavior is agnostic about the specifics of the data being loaded; therefore, some methods remain unimplemented and must be defined by the component that adopts the behavior.
Here’s where our sample data structure proves useful — we’ll assume the server returns data identical to what the static getSampleData method provides.
Let’s recap the provider’s workflow. When initialized, the provider enters the loading state (as previously implemented). It then sends a request to the server. Once the server responds, the _onLoadSuccess method is triggered. Upon receiving the data, we convert it into the desired structure and assign it to the provider’s internal data object. This data object processes the new information and marks itself as modified using the setChangedAll method. The _updateSomethingWhenChanged method then transitions the provider to the ready state. As the provider’s state changes from loading to ready, the _onAfterChanged method triggers a koi-changed event, informing other components that the provider is now ready to supply data.
What can this provider do?
- Self-load: The provider can autonomously fetch data, while subscribed components remain in a waiting state.
- Load and share data: The provider can retrieve a set of records, enabling components to display column headers and even the records themselves.
- Reload: If the provider reloads (via the attemptReload method), components will again transition to a waiting state. Once the reload is complete, the provider will update the components with a fresh dataset.
For now, this functionality is sufficient. Let’s put the provider to use.
Displaying the Provider’s State
We’ll start by designing a component to display the provider’s state. This component will show notifications when the provider is busy loading data or when an error occurs. In its normal state, it will display the data provided by the provider.
For the base class, we’ll use KoiPanel. Why this class and not a table-like class?
As previously noted, the KoiCom library classifies components based on behavior rather than appearance. The required behavior for our component includes reflecting the provider's state by showing notifications for loading, errors, or readiness. Additionally, the component should display table headers.
Should this component manage the table rows? Given that rows will not only be bulk-loaded but also added, deleted, and updated individually, there are two options: either the component handles complex row management itself, or it delegates this responsibility to another component.
We’ll choose to delegate row management to a secondary component. Our main component will include the additional behavior of displaying this delegated component.
In conclusion, the behavior of this component is markedly different from that of a KoiTable. It’s better described as a headered frame — a component that frames displayed rows, includes column headers, and shows notifications when no content is available. We’ll call this class CRUDSampleHeaderedFrame.
We’ll base our component on KoiPanel because it’s a versatile and universal foundation. Additionally, we’ll incorporate the KoiControlConnectorInteractable behavior, as CRUDSampleHeaderedFrame needs to reflect the provider's state.
Do we need to develop a custom behavior specifically for CRUDSampleHeaderedFrame? Likely not. Since the CRUDData type is derived from KoiListData, and functionalities like adding, deleting, and modifying individual records won’t be required, the standard KoiSingleConnector will be sufficient. It will also support the standard KoiControlConnectorInteractable behavior for interacting with the provider.
That said, the component’s socket will have some unique characteristics. For notifications, we will use a collection of div elements, each containing a different message, and toggle their visibility to reflect the current state effectively.
The presence of a set of div elements means that we cannot use a KoiSingleSocket and would be better off with a KoiCompositeSocket, as it allows us to reference multiple internal components by name.
The component will not interact with the socket directly; instead, it will use the CRUDSampleHeaderedFrameSocketConnectable behavior, which will delegate the task of displaying states to the socket.
In the normal state, i.e., when the data is ready for display, the socket should show the table header. To display the header, the socket doesn't need the data loaded by the provider, but it should understand the structure of that data. Information about the data structure can be retrieved from the static getHeader method of the data element. If we were designing a universal socket, we would pass this information to the socket via the component's constructor. However, in this case, we can directly call this static method from within the socket.
The next task is how to display the table rows. As previously mentioned, we will delegate this responsibility to the CRUDSampleTableContents component, which can add, remove, and modify its internal components.
This component will also need to be connected to the data provider, which requires passing the provider’s identifier to the component’s getTag method. This is achieved by modifying the socket's constructor.
Displaying Table Rows
The CRUDSampleTableContents component, responsible for displaying table rows, needs to react to events from the provider. If the provider receives data and is ready to supply it, the component should clear all previously displayed rows and render the new ones.
Additionally, the CRUDSampleTableContents component should provide a method for adding rows to the currently displayed set.
This type of manipulation with internal components can be implemented using the KoiExpansionPanel component, which serves as an excellent foundation for building a row display component.
Unlike the CRUDSampleHeaderedFrame, the CRUDSampleTableContents component needs to understand the structure of the data it receives. For instance, it should be aware of the number of records in the provided data. To achieve this, the component implements the KoiControlListDataConnectorInteractable behavior, which includes the _getItemsFromConnectorData method.
To display the connector’s data, the component doesn’t require its own data. Instead, in the _updateSocket method, it will request the connector's data and pass it to the socket, instructing the socket to clear its contents before rendering the new rows.
Consequently, the socket must offer two methods: one for clearing its contents and another for displaying a set of rows.
In the expandSocketWithArray method, the socket uses the expandSocket method to add rows but transforms the data into a set consisting of item_id, item_name, and deleted. While functional, this approach is less than ideal because the socket shouldn’t need to understand the structure of the records. A better approach would be for the socket to pass the entire record directly to the row component without transformation. This would make the component more flexible. However, to demonstrate how data can be passed to a component via tag attributes, I’ll retain this approach for now — please disregard the limitation for the moment.
Displaying an Individual Row
What constitutes the content of a table row? It will be a component that includes a provider for modifying and deleting a record, as well as a component that displays the outcomes of these actions. The row itself has a single purpose: to contain these two components. In this case, the row can be represented by a simple div.
Since we won’t create a dedicated row component, the placement of the record provider and the component responsible for displaying the row’s content will be managed by the socket of the CRUDSampleTableContents component.
Record Data Object
Before designing the record provider, it’s essential to define the structure of the data object responsible for storing and modifying the record's data.
When interacting with a record, we use its identifier to locate the record, modify the name field, and mark it as deleted using the deleted field. In the table, only these three fields are displayed. As such, the required structure of the record data object comprises item_id, item_name, and deleted. These three elements define the minimal structure necessary for the record data object.
Any component implementing the KoiDataCapable behavior and utilizing this data object will automatically retrieve values from tag attributes, convert them to the appropriate data formats, and apply them as default values.
However, while attributes representing KoiDataElementInteger and KoiDataElementBoolean can be passed directly without issues, string values may contain special characters. Directly including such values in attributes could result in errors. To handle this, the item_name field will leverage the KoiJSONable behavior, which provides methods to safely encode and decode values for attribute use.
For instance, when developing the row provider, these encoding and decoding methods can be utilized in the tag generation process.
Although passing data through attributes is not an ideal approach, it serves as a convenient demonstration. In a production environment, I would prefer to use the KoiTransmitter component for data handling. However, for the sake of this example, we’ll proceed with attribute-based data passing.
The KoiData class does not include methods for retrieving and updating the values of its data elements. It is expected that these methods, for ease of use, will correspond directly to the names of the data elements. These methods must be explicitly implemented within the specific data object.
Such methods are essential for the record provider, as they allow it to define functionality that not only modifies the data but also emits signals when changes occur.
Record Provider
The record provider is designed to simulate the modification and deletion of records. To enable this functionality, we will implement behavior similar to the data-loading behavior previously defined for the main provider.
For now, we’ll operate under the assumption that the record provider will handle only one request at a time. To ensure this, the provider's state will be checked before initiating a request. If the provider is already busy, the request will not be sent.
When a result is received, the provider will update its internal data and trigger an event. The row content display component, which is subscribed to this event, will then update its display to reflect the changes.
Row Content Display Component
The CRUDSampleTableRowContents component is tasked with displaying the state of the record provider. If the provider is in the process of waiting for a server request result, the component should show a loading notification. This can be implemented in the same manner as before, using a set of
This component does not maintain its own data. Instead, it relies on the provider's data for display purposes. Furthermore, if the deleted field in the data is true, the component's socket will update the visibility of the component accordingly.
This introduces a situation where data exchange occurs between the provider and the component, necessitating a shared understanding of the data structure. While the provider inherently understands the data structure due to ownership, the component must rely on an interface to access this information. This interface is provided by a connector. By implementing the CRUDItemDataConnectorInteractable behavior, the component gains the ability to use the _getItemPropertiesFromConnectorEventDetail method to interact seamlessly with the provider's data.
Connector for the Record Provider
While standard connectors for data objects are already defined in the KoiCom library, a specialized connector needs to be implemented for the provider working with the record data object.
This connector will enable the component to access the provider's event_details object, which contains a reference to the provider's data object. Additionally, we will introduce the CRUDItemDataConnectorInteractable behavior, allowing the component to seamlessly interact with the provider's data object.
This setup ensures that the component has access to behavior-specific methods. These methods, in turn, utilize the provider's data object methods, which are defined thanks to the integration of the CRUDItemDataConnector. This structured approach clarifies the origin and functionality of methods like getItemProperties, making their purpose and usage intuitive and easy to grasp.
Modifying a Record
The controls for editing a record will be housed within the row content display component. This creates a two-way relationship between the component and the provider. On one hand, the component updates its display when the provider triggers an event indicating that its data has been modified. On the other hand, the component sends commands to the provider to update the data.
To enable the component to send modification commands to the provider, an appropriate method is required. This method will be defined in the CRUDItemDataConnectorInteractable behavior, with the command transmitted via the connector.
This approach highlights how a connector can serve dual purposes: not only facilitating data retrieval but also enabling provider management, akin to how sockets are controlled.
Next, we will design a component that can invoke provider management methods directly from the row content display component.
Record Control Panel
Returning to the row content display component's socket, we will embed a CRUDSampleControlPanel component in each row.
This control panel will consist of three dialog components. The first dialog prompts the user to select an action to perform on the record. The second dialog confirms the deletion of the record. The third dialog asks the user to provide a new value for the item_name field.
The primary responsibility of the control panel is to switch between these dialogs based on user commands.
Both buttons in the action selection dialog will be directly intercepted by the CRUDSampleControlPanel. Upon processing the events triggered by the user's action choice, the panel will display the corresponding dialog.
Similarly, events triggered by pressing the cancel button in either the delete or edit dialog are also processed. The currently active dialog is hidden, and the action selection dialog reappears.
To enable CRUDSampleControlPanel to intercept events, it must implement the KoiOperationsInterceptable behavior and define the _handleOperated method.
To prevent events that the panel can process from propagating further, it also implements the _stopPropagationWhenOperated behavior.
The panel identifies intercepted events by retrieving the command using the getAction method and comparing it to the action codes provided by the static methods of the dialogs.
Events that the panel cannot process are passed along, encapsulating the dialogs as a cohesive, complex dialog system. This system does not emit events if no changes are made by the user, but it triggers an event containing either a modification or deletion command when applicable.
Since the dialogs themselves are straightforward, we can now proceed to explaining how these two commands are executed.
Modifying and Deleting Records
Commands from the record control panel are expected to be sent to the record provider, which will then handle making the appropriate server requests. This can be achieved in one of two ways:
1. Direct Transmission by the Record Control Panel The control panel could directly issue commands to the provider. However, this is not ideal because the control panel's role does not include interacting with the provider. Adding a connection to the provider solely for transmitting commands would unnecessarily expand its responsibilities.
Relay Through the Row Content Display Panel A better approach is for the row content display panel to receive events from the control panel and forward commands to the provider.
To implement this, we will update the CRUDSampleTableRowContents component to include the KoiOperationsInterceptable behavior.
As the CRUDSampleTableRowContents component already implements the CRUDItemDataConnectorInteractable behavior, it can leverage its existing methods, _attemptChangeItemName and _attemptDeleteItem, to forward commands to the provider.
With this implementation, we have completed the functionality for modifying and deleting records. The next step is to address adding new records.
Add Record Panel
Sending a request to the server to add a record can be handled independently of fetching the list of records. However, it’s not ideal to allow users to add records before the list is loaded.
To address this, the component responsible for adding records should reflect the state of the data list provider. One approach would be to connect this component to the provider similarly to how we connected the CRUDSampleHeaderedFrame panel earlier. However, a simpler solution is to embed the add functionality directly into the CRUDSampleHeaderedFrame component, achieving the same outcome.
In this approach, the add record functionality can be represented by a div that encapsulates the add record panel (containing the add dialog) and the new record provider.
The new record provider works similarly to the record provider but with a few distinctions:
1. Unlike the record provider, the new record provider doesn’t rely on initial values from the loaded list. It can be initialized with an empty string as the default value for item_name.
2. The new record provider receives the data for the newly created record from the server, including a server-generated ID. To simulate this process, the provider requires functionality to mimic ID generation.
The CRUDSampleAppendPanel connects to the new record provider and displays its state, much like the row content display component reflects the state of the record provider.
The add record panel features a standard dialog with a text input field.
This design allows the add record panel to display the provider’s state, capture user input, and emit an event containing the entered value.
Previously, when creating the row content display panel, we modified the CRUDItemDataConnectorInteractable behavior to establish a two-way connection between the component and the provider. This enabled the provider to send a request to the server to modify a record based on user input events. This time, I’d like to demonstrate how to set up this connection through a component that combines the add record panel with the add record provider.
Adding a Record
A two-way connection implies that the events of a component control the same provider from which the component retrieves its displayed data. However, more commonly, a component’s events are used to manage other components.
In such cases, a third component that integrates the provider and the display component should handle event processing. This third component acts as a controller. In our case, the CRUDSampleHeaderedFrame component will serve as the controller, and we will equip it with the KoiOperationsInterceptable behavior.
Upon intercepting an event from the add record panel, the CRUDSampleHeaderedFrame component will extract the user-entered value from the event and pass it to the socket, which then calls the provider’s attemptAppendItem method.
Let’s consider this design. On one hand, we’ve removed the overly tight coupling between the provider and the add record panel, making the connection one-way. On the other hand, we’ve overloaded the CRUDSampleHeaderedFrame component with excessive behavior. If CRUDSampleHeaderedFrame were to implement a wide range of diverse behaviors, it could become confusing. This leads us to the idea of encapsulating such behavior within a separate component inside CRUDSampleHeaderedFrame. I’ll leave this as an exercise for you to explore.
Now, returning to the provider: after receiving the user input, the provider sends a request to the server. Once the response is received, it updates its data and triggers a koi-changed event. At this moment, no component is subscribed to this event, so the new row won’t appear in the table.
We will again use CRUDSampleHeaderedFrame as the controller. For CRUDSampleHeaderedFrame to intercept the provider’s data change event, the event must propagate. However, the koi-changed event currently has its bubbles property set to false. Let’s fix that.
Next, we’ll ensure that CRUDSampleHeaderedFrame subscribes not only to the koi-operated event but also to the koi-changed event.
Finally, we’ll make sure that CRUDSampleHeaderedFrame recognizes this event and instructs the component displaying the table rows to add a row with the values received in the event.
This method works, and I wanted to demonstrate it, but note that by doing so, we’ve made significant changes to existing behaviors, which alters their original intent. For example, the KoiOperationsInterceptable behavior is designed to work with a single data object, but in the _handleOperated method, we’re working with a completely different object. This raises the question of how to solve this problem in a production project without altering the existing behaviors.
I’ll add that if CRUDSampleHeaderedFrame adds a row, it does so in response to a specific command. Currently, CRUDSampleHeaderedFrame reacts to an event from the provider, meaning it knows too much about the provider. If a similar command were to come from another component, we’d need to define yet another similar reaction. To avoid this, it’s necessary to formalize the command and make CRUDSampleHeaderedFrame respond to this command. I’ll leave this for you to consider.
For now, that’s all. I’ve demonstrated what I intended to. The final CRUD component is ready. From here, it can be endlessly improved, refined, and polished.