Web components is a important part of web development today.
Bindable element is a class that extends HTMLElement and allows you to write components that make use of crs-binding.
Though this does create a dependency on crs-binding it also reduces development effort.
Before we get to bindable elements just a couple of quick notes about using stock HTMLElements with some binding engine features.
One of the biggest memory leak issues is around events not being cleaned up.
The bindable element takes care of that using registerEvent.
You can also make use of this in a standard element by using crsbinding.dom.enableEvents(this);
in the connected callback of the component.
Make sure you use js crsbinding.dom.disableEvents(this);
in the disconnected callback.
Disabling the events also cleans up all the events on your behalf so you don't need to worry about it.
When using a bindable element, when you use this.setProperty
it will notify changes on your behalf so that two way bindings can respond ot property changes.
This is not available in a HTMLElement but you can achieve the same thing by dispatching an custom event using the following event name convention.
${propertyName}Change
In other words, if I have a property called value that I want to notify has changed, I can do the following.
this.dispatchEvent(new CustomEvent("valueChange"));
The BindableElement class comes in a separate file to the main binding engine, but since it uses the main binding engine you will still need to include that in your index page as per normal.
Creating a new file for your component you can import it using ES6 modules.
import {BindableElement} from "/node_modules/crs-binding/crs-bindable-element.js";
Once you have that imported you can create your component as per usual
class MyComponent extends BindableElement {
get html() {
return import.meta.url.replace(".js", ".html");
}
get items() {
return this.getProperty("items");
}
set items(newValue) {
this.setProperty("items", newValue);
}
async connectedCallback() {
await super.connectedCallback();
// ... do some stuff here
}
async disconnectedCallback() {
await super.disconnectedCallback();
// ... do some stuff here
}
async preLoad(setPropertyCallback) {
/*
Perform operations that need to be in place before the HTML is parsed.
Typically if you have a once binding, you need to ensure that the values are in place before parsing takes place.
This is where you will do that.
setPropertyCallback(propertyName, value);
*/
}
load() {
/*
... perform initialize of resources as part of the connectedCallback process
but before the ready event is fired.
*/
}
}
customElements.define("my-component", MyComponent);
The above example represents the basic structure for a web component using BindableElement.
Things to take note of:
- html getter defined where to find the html file to use as innerHTML of the component. If you don't want your innerHTML overwritten or you don't have a html file you need to return null in this getter and mange that yourself.
- The element instance is used as the context for the parsing so all binding expressions are relative to the element's class.
- connectedCallback and disconnectedCallback differs a little from the standard as these are async. If you are not loading a html file you can take the async off.
- Once the component ready it raises a event "ready"
There may be cases when you don't load a HTML file for your component. It may be that you will create the UI manually or via some schema generation process. In such cases you will need to parse the element when you are ready.
crsbinding.parsers.parseElements(this.children, this);
this.dispatchEvent(new CustomEvent("ready"));
Only call the ready event when the component is actually ready.
Here is a example of a property getter and setter.
get items() {
return this.getProperty("items");
}
set items(newValue) {
this.setProperty("items", newValue);
}
Note that it uses getProperty and setProperty.
This is only required if the property of that element is being used for binding purposes.
On a custom element, some properties may be used for binding and others used for internal use.
The binding engine stores the data in the binding store.
getProperty reads data from the store and returns that too you.
setProperty writes data to the store.
If a property is used for internal use you don't need to save those values in the store.
Only items in the store is available for binding expressions to make use of.
It is important to note that by default here that there are no listeners attached to properties or objects.
This no proxies or observers.
When updating the values on the binding store using set property, it then performs all the required updates needed.
If you have a html file with your component, you can register events in the HTML as per normal. You don't always want to have a html file with your compoonent though. Registering events often lead to memory leaks because people forget to unregister them.
Bindable element has a function called registerEvent were you can register a event and on disposal of the component, the component will clean that up.
this.registerEvent(this.querySelector("[role='tablist']"), "click", this._click.bind(this));
You can also unregister events if you need finer grain control using unregisterEvent
.
unregisterEvent uses the same prameters as register.
It is important to note that .bind
creates a new function so you can use that in unregister event.
You will need to do something more like this.
// register
const handler = this.click.bind(this);
this.registerEvent(element, "click", handler);
// unregister
this.unregisterEvent(element, "click", handler);
handler = null;
There are times when you don't want to share context ids between components.
To prevent the component from registering with the binding engine on it's own you need to override the hasOwnContext
property.
get hasOwnContext() {
return false;
}
The components field _dataId
needs to be set during construction.
This means you need to load the component after the component who's id you wish to share has been loaded.
constructor() {
super();
this._dataId = this.parentElement.viewModel._dataId;
}
In the above example we are getting the view model's _dataId to use.
If you were using a parent bindable element you can just do the following.
this._dataId = this.parentElement._dataId;
To ensure that the components load after the parent is ready see the following example.
async connectedCallback() {
await super.connectedCallback();
await import("./comp1.js");
await import("./comp2.js");
}
Please note that you must define event functions on the class that is the origional context for that id. In other words the parent view model or element.
When you remove the element from the DOM, it will call the disconnectedCallback as per usual.