In this segment, we delve into the handling of events within the context of crs-binding
. Many features are designed to respond to DOM events and carry out a corresponding action when triggered.
The initial step involves parsing the markup to identify what’s known as intent definitions. These definitions are paired with a provider, an entity adept at deciphering the specific intent and executing the appropriate action upon activation.
Consider the “click.call” instance, where “click” denotes the DOM event, and “call” signifies the intent type. It’s the duty of the call provider to undertake the parsing, comprehension, and setup of the intent, preparing it for subsequent execution.
All identified intents are cataloged within the event store. You’re able to explore this event store directly within your browser by inspecting the crs.binding.eventStore
object via the console.
The event store incorporates a lookup table, utilizing the event name as the key. In this case, we opt for a standard dictionary (an object literal), as it provides quicker lookup times compared to a HashMap. However, in other programming languages, a HashMap might be used for this purpose. Within this structure, there’s another level of lookup that uses the UUID of elements subscribing to that event. These entries include an array of intents because it’s possible for the same event and element to trigger multiple intents simultaneously. For instance, one could initiate a call on a binding context and simultaneously set a global variable using the setProperty intent.
The store data structure
{
"click": {
"573671f8-4f6c-4b26-951f-af765aa5a539": [
{
"provider": ".call",
"value": "onEvent($event)"
}
],
"387e48a3-6060-4283-a10d-ada545961b35": [
{
"provider": ".attr.toggle",
"attributes": [
"aria-expanded:true"
],
"queries": [
"this"
]
}
]
},
"change": { ... },
"keyup": { ... }
}
In the provided example, two separate intents are associated with the click event, each corresponding to different elements. Every provider maintains a unique data structure within this system, tailored to meet the requirements necessary for executing its respective intent.
It’s important to note that the specific provider associated with each intent is also recorded. This recorded information streamlines the process of identifying which provider should receive a given intent definition for execution. The providers’ role is to unravel the intent and ensure that its execution aligns with the predetermined specifications.
When an element is unmarked for disposal, the system scans this data structure for entries matching the UUID of the element being removed and then eliminates the corresponding intent. If, as a result, an event no longer has any intents linked to it—contrary to the earlier example where multiple intents were associated with a single event—the now-empty event lookup table is purged from the system.
Overview of the providers
Let us look at this from the provider’s perspective.
If you want to create your own provider, there are a number of things we want to take note of.
export default class CallProvider {
async onEvent(event, bid, intent) {
await execute(bid, intent, event);
}
async parse(attr) {
parseEvent(attr, this.getIntent);
}
getIntent(attrValue) {
const result = { provider: ".call", value: attrValue }
getQueries(attrValue, result);
return result;
}
async clear(uuid) {
crs.binding.eventStore.clear(uuid);
}
}
The section describes the operations of a provider that handles “.call” expressions, a common pattern among event-based providers.
- parse: This function is triggered during markup parsing. It takes an attribute, constructs the intent, and registers it with the event store.
- getIntent: This is where the intent, which will be stored in the event store, is constructed.
- clear: This function signals to the event store that an element is no longer in use. The store is then instructed to eliminate any references to this element.
- onEvent: This function is called when the event is triggered. It receives the event object (such as a PointerEvent), the binding context ID, and the intent definition previously stored in the event store. The
execute
function within this method carries out the actual intent execution.
Here are some actual source examples for you to evaluate and compare.
The processes across these functions are notably similar, with key distinctions lying in:
- The construction of the execution event, which is derived from the attribute value.
- The enactment of the intent at the moment the event is triggered.
Managing events
The event store’s function is to keep track of events. Handling a large number of events can be challenging due to the increased memory usage, and there’s a higher risk of encountering memory leaks and timing problems, especially as they relate to the lifecycle of the element.
Upon registering an event intent in the store, an event is also set up on the DOM, typically bound to the document just once. The event’s handler delegates the task of execution to the provider. This approach significantly cuts down on the total events registered and ensures no lingering references to elements, enhancing their disposal and garbage collection. Global management of events facilitates smoother element lifecycle management and memory efficiency.
Handling elements within the shadow DOM is more complex due to the difficulty events face in piercing through the shadow root’s encapsulation. crs-binding
operates under the presumption that shadow DOM elements employ BindableElements, which possess a “registerEvent” method. This method is advantageous over the standard “addEventListener” because it upholds an event dictionary on the element itself. Consequently, when the element is detached from the DOM, the associated events are automatically tidied up, easing the cleanup process.
Should you choose not to utilize BindableElement for your component, it will be necessary to implement a “registerEvent” method within your component to handle memory management independently. Alternatively, you could incorporate the mix-in employed by BindableElement. To do this, integrate it within the constructor of your component.
crs.binding.dom.enableEvents(this);
And on the disposal of your element call
crs.binding.dom.disableEvents(this);
By setting up the necessary mechanisms for event management, this process alleviates you from the intricacies of cleanup, provided you ensure to invoke “disableEvents” when necessary.