A complete component
Three menus, one packaged component, one item template supplied by the page. Each instance gets its id from
$key, loads its own data and carries b-observe="data" so it re-renders when its
own array changes.
b-observe is not required for a component to pick up a change, it works without it too, but it
is the efficient path. Without it, Bindary finds the change by walking the whole tree from the root and
re-deriving this component's context along the way. With it, the change is captured against this exact
item's own tree state and only that component's own view gets updated, nothing else has to be walked to
find it.
includeId in data-settings exists only to demonstrate that a component can be
handed an includable template from the page, which is what the
itemTemplate span below is for. It is not something every packaged component needs.
b-include and b-component alone are enough to use one; skip itemTemplate
and includeId entirely unless your component specifically wants to render markup the page supplies.
Every connected instance also adds itself to a module-level array, closed over inside the component's script
and exposed as window.itemListInstances, in the order it connects. That is how code outside the
component, the page's controller in this case, can reach a specific instance:
window.itemListInstances[0] is always the first menu, [1] the second and so on.
<span id="itemTemplate" b-includable b-value="$value"></span>
<div b-repeat="menus">
<h4 b-value="title"></h4>
<div b-include="file:/item-list.html" b-component="itemList"
b-load="$component.load" b-post-render="$component.postRender" b-observe="data"
data-settings='"id":"menu_{{$key}}", "includeId":"itemTemplate"'></div>
</div>
Component code
// on the component
load(templateItem)
{
const attr = templateItem.element.getAttribute('data-settings');
this.settings = attr ? JSON.parse('{' + attr + '}') : {};
this.componentId = this.settings.id;
this.includeId = this.settings.includeId;
// populate this instance's items from its own id, e.g. "menu_0" -> menus[0]
const key = this.componentId.split('_')[1];
this.data = $bindary.appData.demo.component.menus[key].data;
}
<!-- item-list.html -->
<html>
<head>
<script id="itemListJS" type="text/javascript">
if (!customElements.get('item-list'))
{
const instances = [];
class ItemList extends HTMLElement
{
constructor()
{
super();
this.settings = {};
this.componentId = null;
this.includeId = null;
this.data = [];
this.extra = [];
this.selected = null;
this.events = {
onPostRender: $.createEvent('onPostRender'),
onClick: $.createEvent('onClick')
};
// registered once per instance; load() can run again on re-render,
// and re-adding the same listener there would fire it multiple times per event
this.addEventListener("NewData", (e) =>
{
// extra.length is read live off the instance, reflecting the push that
// already happened before this handler ran
alert(`${e.detail.eventPayload.message} (extra now has ${this.extra.length} items)`);
e.preventDefault();
});
}
connectedCallback()
{
if (!instances.includes(this))
instances.push(this);
}
load(templateItem)
{
// settings live on the including element, not on this one
const attr = templateItem.element.getAttribute('data-settings');
this.settings = attr ? JSON.parse('{' + attr + '}') : {};
this.componentId = this.settings.id;
this.includeId = this.settings.includeId;
// populate this instance's items from its own id, e.g. "menu_0" -> menus[0]
const key = this.componentId.split('_')[1];
this.data = $bindary.appData.demo.component.menus[key].data;
// data itself is watched automatically via b-observe="data" on the including element.
// extra demonstrates the manual alternative, see 'Manually observing a change instead' below.
const event = new CustomEvent("NewData", { detail: {}, cancelable: true });
this.extra = [];
this.extra = $bindary.observe(this.extra, 'extra', {
event: event,
element: this
});
}
postRender(templateItem)
{
this.events.onPostRender.fire(this, templateItem);
}
click(templateItem)
{
const el = templateItem.element;
if (this.selected)
{
const previous = document.getElementById(this.selected);
if (previous)
previous.classList.remove('selected');
}
el.classList.add('selected');
this.selected = el.id;
this.events.onClick.fire(this, templateItem);
}
}
customElements.define('item-list', ItemList);
window.itemListInstances = instances;
// registered once, outside any lifecycle callback. The return value is derived
// from item.element on every call, so it's correct no matter how many times, or
// when, Bindary calls it, this component may be re-resolved on later render
// passes if it sits inside a repeat without b-observe.
$bindary.addComponent('itemList', (item) => item.element.querySelector(':scope > item-list'));
}
</script>
<style id="itemListCSS">
item-list > ul
{
margin: 0;
padding: 0;
list-style: none;
cursor: pointer;
}
item-list > ul > li
{
padding: 0.35rem 0.6rem;
border-bottom-style: solid;
border-bottom-width: 1px;
}
item-list > ul > li:last-child
{
border-bottom-style: none;
}
item-list > ul > li:hover
{
background-color: rgba(127, 127, 127, 0.12);
}
item-list > ul > li.selected
{
border-left-style: solid;
border-left-width: 3px;
font-weight: 600;
}
</style>
</head>
<body>
<item-list>
<ul>
<li b-repeat="data" b-include="includeId" b-attributes="id = $value" b-on-click="$component.click"></li>
</ul>
</item-list>
</body>
</html>
The page's controller reaches a specific instance through window.itemListInstances:
ctrl.addCourse = () =>
const count = window.itemListInstances[0].data.length;
window.itemListInstances[0].data.push(`Extra starter ${count}`);
Result
Adding to the first menu's array adds one row to that list. The other two are untouched and the component
was not told anything happened.
Manually observing a change instead
b-observe="data" above keeps the view in step automatically, no code of your own required.
For a property you own, you can go further: register a CustomEvent and hand it to
$bindary.observe() and you get notified on every change to react however you want, log it,
transform it, forward it elsewhere, anything. The same component wraps a second property,
extra, this way instead of using b-observe.
let event = new CustomEvent("NewData", { detail: {}, cancelable: true });
this.addEventListener("NewData", (e) =>
{
alert(`${e.detail.eventPayload.message}
(extra now has ${this.extra.length} items)`);
// explicitly cancel the event to cancel the change registration
e.preventDefault();
});
this.extra = $bindary.observe(this.extra, 'extra', {
event: event,
element: this
});
Calling preventDefault() here is just to show you have that option: by explicitly canceling
the event, you cancel the change registration and the auto view update that comes with it. Skip it and
extra behaves exactly like b-observe="data" above.
Register the listener once per instance, in the constructor, not in load(). Bindary can call
load() again for an already-connected instance, whenever the include is reloaded by a
re-render higher up and addEventListener does not deduplicate identical handlers. Registering
it in load() stacks a second listener on the same element, so one dispatched event fires the
handler twice, three times after the next reload and so on. The event object itself can still be created
fresh in load(), only the listener needs to live outside it.
The button below pushes an item onto extra, which fires the NewData event and
the alert shows the new count.
ctrl.addExtraItem = () => window.itemListInstances[0].extra.push(`Extra item`);