Understanding the binding context

The binding context store is a crucial component of the binding engine, serving as the hub where binding contexts are generated and maintained. It also oversees the triggers for updates when there are changes within context properties. While it’s not exceedingly complex, it demands careful attention to grasp the interplay of its various parts.

The binding data manager is accessible via crs.binding.data. It contains several key properties:

  • data: This stores the actual context data.
  • elementProviders: Identifies which providers are associated with an element, as specified in its markup. This is used to determine which providers to invoke during element updates.
  • context: A lookup table using the binding context ID (bid) as the key, with the corresponding context object instance as the value. Typically, this would be an HTMLElement, but it is not restricted to it. For execution of methods on a specific context, such as with the “.call” provider, the system retrieves the object linked to the bid and invokes the method on it.
  • callbacks: This defines what elements must be updated when a property path is updated on the binding data.

Context data

A key distinction of crs-binding compared to other binding engines is its approach to handling binding expressions. Rather than modifying properties on an existing object, crs-binding stores its binding context data independently. This separation mitigates complexities associated with object lifespans and timing issues. It also enables the creation of binding contexts for non-visual features while retaining many of the same benefits. Consolidating the context, its binding data, and related functions in a single location streamlines the codebase, facilitating easier management.

{
    "0": {
        "name": "global",
        "type": "data",
        "data": {
            "menuVisible": true
        }
    },
    "4": {
        "name": "BindingViewModel",
        "type": "data",
        "data": {
            "title": "Binding",
            "person": {
                "firstName": "John",
                "lastName": "Doe",
                "age": 30
            },
            "greeting": "Welcome to one-way binding",
            "people": [
                {
                    "firstName": "John",
                    "lastName": "Doe"
                },
                {
                    "firstName": "Jane",
                    "lastName": "Smith"
                }
            ]
        }
    }
}

The lookup table in our example uses the binding context ID (bid) established during context registration, and it includes some standard properties:

  • name: Intended for debugging, this represents the context’s identifier. By default, it’s the class name of the instance for which the context is registered. In our case, there’s a “global” context, and “BindingViewModel” is the name of the class where this context originates.
  • type: This is set to “data” by default, with provision for future expansion to accommodate different types of context data.
  • data: This is the foundation of the binding data. Any data paths defined are in relation to this data object, although in practice, they’re often considered relative to the context object, such as the corresponding element. Since the data isn’t actually stored on the element, it’s relative to this data object. Property paths, like “person.firstName,” are navigated using the utility function “crs.binding.utils.getValueOnPath” to extract the value from the data object. It’s important to note that the data structure is a standard object literal, meaning it supports the data types that JavaScript itself supports.

Callbacks

This is a lookup table where the bid is the key. The value here is another lookup table, wherein the key is a property name within the binding context, and the value is a collection of elements that need to be updated when that property changes.

{
    "4": {
        "title": [
            "796b8ccf-d4fd-4372-b07b-c6e7e6c720b3"
        ],
        
        "person.firstName": [
            "6bb58b6e-7737-46a9-97aa-67f6a7888ae8", 
            "b24ed309-4436-423f-b15d-42c45a5bbe89"
        ],
        "person.lastName": [
            "c4f4d149-422a-4b1a-bd9e-cbfbc6600b2a", 
            "b24ed309-4436-423f-b15d-42c45a5bbe89"
        ],
        "person.age": [
            "26ea6907-51cc-41ef-8667-dd8e4f62f69f"
        ],
    }
}

In the given scenario, the callbacks object contains a binding context ID (bid) with a value of “4,” indicating the presence of one binding context. In a complex enterprise application, there could be numerous active contexts on the screen at any given time, corresponding to various components and the UI framework elements like menus and global toolbars.

Within our binding context object, there are several property paths under observation:

  1. title
  2. person.firstName
  3. person.lastName
  4. person.age

For the properties ‘title’ and ‘age’, there is only one element that requires updating when their values change. However, ‘firstName’ and ‘lastName’ have two elements each that need updating. This typically happens because, besides the input element, there is also a summary element (such as a div) that might display a combined statement like “Current user: ${person.firstName} ${person.lastName}”.

The ‘firstName’ and ‘lastName’ share a UUID, which begins with “b24e…”. This shared identifier implies that if either the ‘firstName’ or ‘lastName’ changes, the element associated with this UUID will update to reflect the new information.

elementProvider

This data structure functions as a lookup table to optimize performance. When a property path undergoes a change, we can identify which elements require updates through the callbacks, but the specific providers acting on those elements remain unknown. That’s where this lookup table proves useful: it uses the UUID of an element as a key and a set of providers as its value.

This setup enables us to swiftly determine the types of changes that need attention. It’s important to note, however, that not all providers listed may be connected to the property change in question, but this system still narrows down the field for our decision-making process.

{
    "2bac5fa6-f68c-424d-9318-953f196e131e": [".bind", ".attr"]
}

getProperty

When you have a BindableElement you normally get a binding context value using the getProperty method.

const value = this.getProperty("person.firstName");

Internally, it invokes the getProperty method of the binding context manager. The main distinction is that when utilizing the version from the binding context, you must provide the binding context ID as the initial argument. This method can be accessed directly, and in scenarios where you need data from a different context and know the respective context ID, this is the approach you would use to retrieve the necessary information.

crs.binding.data.getProperty(4, "person.firstName")

The workflow behind this is fairly simplistic.

  1. Get the binding context data object for the given bid
  2. Use crs.binding.utils.getValueOnPath to retrieve the value on that object with the property path specified.

This method is designed to be synchronous, aiming to maintain optimal performance. The internal operation is a straightforward value retrieval, which does not necessitate any asynchronous behavior.

setProperty

When you have a BindableElement you normally set a binding context value using the setProperty method.

await this.setProperty("person.firstName", "John");

Similar to getProperty, this function internally calls the version from the binding context, appending the binding context ID (bid) as a parameter.

await this.setProperty(4, "person.firstName", "John");

The setProperty workflow is somewhat intricate:

  1. Retrieve the existing value from the binding context and temporarily store it.
  2. If the value is an array, encapsulate it within a proxy to observe changes.
  3. Access the binding context’s data object and assign the new value to the specified path. If the path includes nested objects, such as “person.firstName,” and the “person” object does not yet exist, it is created and the “firstName” property is set with the new value.
  4. Update the UI by examining the callbacks object for any related triggers. If updates are necessary, they are carried out accordingly.
  5. If the context object includes a method named after the property change pattern, e.g., “firstNameChanged”, invoke this method first with the new value, followed by the old value.
  6. Should the context object possess a “propertyChanged” method, call it with the property name, new value, and then the old value, in that order.