It is finally time to look at binding expressions.
Before we get into the details of it we need to first be clear as to what kind of binding crs-binding supports.
- Once off binding that sets the initial html value on startup and then never again.
- One way binding that updates the DOM with changes on the model
- Two way binding that is commonly used when you don't want to just update the DOM but also the model should element properties change. Think inputs as a example.
- Conditional binding
- Style binding
Binding on the DOM primarily sets the element's property.
So when I bind to the value of a input it updates the value property and not the attribute.
You can however bind specifically to an attribute but we will deal that particulars about that in that section.
You can also bind to events on elements that trigger functions on the model. This will become more clear when we look at examples.
There is also what we call inflation and though this is DOM update work, it is technically not binding event though that is a crs-binding feature. More detail on that later also.
<h2 inner-text.once="title"></h2>
<input value.once="title" />
In these examples, the values will be copied from the context to the DOM but there are not listeners set up to keep it up to date. This is an memory efficient binding that should be considered for static items such as labels, headings ... The input example here is not really practical, but just a example of syntax.
This binding will copy data to the DOM each time the value changes.
There is the initial copy and then each time the property changes.
There are a number of ways you can express a one way binding.
// using inner text binding expressions
<div>${firstName}</div>
<div>My name is: ${data.firstName}</div>
// using binding attributes on data- attributes
<div data-name="${data.firstName}"></div>
// using the .one-way postfix on a given property you want to set
<input value.one-way="data.firstName" />
When you use the postfix method, you are explicitly telling the binding engine how to treat the binding. In those cases you don't need to use the string literal "${}" syntax. In the cases of the innerText the binding engine needs to understand where to put the values and what is just text. Here we reuse the string literal / template syntax from JavaScript as if you are formatting text. The same applies to the attribute binding. If the binding engine spots the "${}" syntax in the text it treats it as a one way binding.
Two way binding inherits from one way but also adds event listeners to update the model when DOM changes happen. This is mostly used when binding with inputs, selects ... or custom elements that need to push data back to the model.
<input value.two-way="data.firstName" />
You can also use the .bind
syntax
<input value.bind="data.firstName" />
This syntax is a bit more tricky but can also be very convenient. If the attribute you are defining on is either "value" or "checked" it will automatically use two way binding. If not it will use one way binding.
<input checked.bind="isVisible" type="checkbox">
<select value.bind="selected">
<option value="0">Option 0</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
There are cases such as svg where using an attribute binding like x="${position.x}"
will produce a syntax error because the text "${position.x}" is not a valid value for that attribute. In those cases you should instead use the ".attr" syntax for binding to the attribute.
<svg height="100%" width="100%" xmlns="http://www.w3.org/2000/svg">
<rect x.attr="${position.x}" y.attr="${position.y}" width.attr="${size.width}" height.attr="${size.height}" fill="#ffbb00"></rect>
</svg>
This is an attribute binding meaning that it updates the attributes of an element and not the property.
In these bindings you define a boolean expression. The syntax will determine one of two things.
- Set the attribute with a value
- Set the attribute or remove it
A common use case for this is to show or hide a element by binding the hidden attribute to an condition.
<div hidden.if="isActive == false">Some Text</div>
In the above example if the property isActive is false attribute will be present and if not, it will be removed
<div data-value.if="isActive == true ? 'Yes'"></div>
In this example if the expression passes the data-value is set to a string value "Yes" but if it fails the attribute will be removed.
<div data-value.if="isActive == true ? 'Yes' : 'No'"></div>
This example has a pass and fail value. Either way the attribute will be set depending on success of the expression.
When the isActive property on the context changes the expression will be tested again and the required updates made.
Style binding takes on two forms
- Updating the class list
- Updating the style property of the element
Style binding looks and operates the same way as conditional attribute binding in that it uses the if
syntax and also a condition.
This binding uses the attribute classlist
.
You can still use the class attribute freely.
<div classlist.if="isActive == true ? 'green'"></div>
In this case the class 'green' will be added to the element's class list if the expression passes.
You can also add and remove multiple classes in the expression.
<div classlist.if="isActive == true ? ['green', 'italic'] : 'red'"></div>
The style binding can bind to a property on the binding context or using the if
expression.
<div style.background.one-way="background"></div>
When the background property on the context changes the element's style.background property will be set to that value. It should thus be a valid CSS value.
With regards to the if
expression I think by now you get the point so instead of belaboring it here are two examples.
<div style.background.if="isActive == true ? 'blue'"></div>
<div style.background.if="isActive == true ? 'blue' : 'red'"></div>
Working with transforms is a bit different in that you have css functions you can call.
transform: translate(100px, 200px)
To set these as binding expressions you need to declare them as you would in javascript using string literals.
<div style.transform="`translate(${pos.x}px, ${pos.y}px)`"></div>
You can also perform some calculation for example:
<div style.transform-origin.one-way="`${(rect.x / 2)}px ${(rect.y / 2)}px`"></div>
Please note in the above example the calcuation is done between "(" and ")".
If you don't add this, you will get a error that the string literal is not valid.
Under the hood it uses a sanitised version of that string literal so normal string literal rules apply.
HTMLElements have events and when a event occurs you want a function to be called on the binding context. The syntax for this is the event's name followed with ".call".
<button click.call="btnClicked">Normal</button>
<button click.call="btnClicked($event)">With Event</button>
<button click.call="btnClicked('hello world', $event)">With Parameters</button>
Above are three examples.
All of these will call the btnClicked
function when clicked.
The first example will not pass in any parameters.
The second will pass the mouse event and the third a static text and the mouse event.
These features are available from version "0.0.120"
Event bindings as seen above fires functions on the current context.
What if I wanted to fire a function or notify of intent not on the current context.
This is where event aggregation comes in. See the event aggregation section for more details.
We will here show you now to declare a binding for the emit and postMessage features of the event aggregation.
For these examples we are using click events but any event will work.
This is the equivalent of crsbinding.events.emitter.emit
.
<button click.emit="customEvent(title='hello world', $event, $context)">Custom Event</button>
<button click.emit="customEvent(title='hello world', $event, contacts=${contacts})">Custom Event - contacts</button>
The syntax is the same as the events section but in this case you are not calling a function by the "customEvent".
Instead "customEvent" is the key used during the subscription of the event aggregation.
The parameters defined between "( and )" define the properties sent on to callback registered on the aggregation.
There are three types of parameters.
- Actual values. e.g. "hello world"
- Keyword $event and $context
- Value path from current context. e.g. ${contacts}
If the parameter is $event it will create a property called "event" on the args object with the event object.
If the parameter is $context it will create a property called "context" that contains the context of the event.
As per normal binding you can define a path relative to the current context using ${property path}. You will notice that the value and path bindings are precede by "propertyName=". This is used to define the property name on the args object.
The args object would as a result look like
{
title: "hello world",
event: eventObject,
context: contextObject
}
and
{
title: "hello world",
event: eventObject,
contacts: propertyValue
}
This also applies to the post binding expression. The differences will be explained in the post section.
This is the equivalent of crsbinding.events.emitter.postMessage
.
<button click.post="got-contacts['input-contacts', 'input-form'](title='hello world', $event, contacts=${contacts})">Post Message</button>
This is the same as the emit with one exception.
You need to define the query of the objects you want to send the message to.
This is defined between the "[ and ]" brackets.
You can think of it as an array query strings.
For this to work you need to have a onMessage function on the elements.
See the event aggregation documentation for more details.
One of the details where this differs from calling it directly is that key provided "got-contacts" will be a on the args object as the property "key". Should you call postMessage directly, you are in charge of the object structure.
You don't need to provide all the details.
Sometimes all you need is a key and in that case you can do something like this.
click.post="close['dialog-component']"
<button click.post="got-contacts['input-contacts'](title='hello world', $event, contacts=${contacts})">Post Message</button>
When you click this button send a message to the component input-contacts'. The component's
onMessage` function will be called and passed on the parameters as defined between the ( and ) of the expression.
By default the system has a global binding context.
Lets say I have a indicator in header to show the number of messages I have.
I don't want to directly couple my messages manager to the binding engine but I do want changes in the quantity to show on the UI.
A easy way to do that is to have a globals property for the number of messages that you can bind against.
Binding to globals work the same way as the normal binding expressions but with a $globals
keyword.
<div>${$globals.messagesCount}</div>
You can set this value from anywhere in your code using the setProperty function.
crsbinding.data.setProperty(crsbinding.$globals, "messagesCount", 2);
Globals are typically used for binding expressions that live the lifetime of the application and is not bound to a particular context but cross context. If the binding expression is not for the lifetime of the application but just between components, you can consider using the event aggregation instead.
You can also set globals on events of components using setValue binding expression.
<button click.setValue="$globals.menuVisible = !$globals.menuVisible">Menu</button>
<button click.setValue="$context.menuVisible = !$context.menuVisible">Menu</button>
The above example toggles a boolean value on the globals, showing and hiding a menu UI.
Here is a example of UI being affected by the above toggle change.
<nav style.visibility.if="$globals.menuVisible == true ? '' : 'hidden'">
<ul>
<li><a href="#example">Example</a></li>
</ul>
</nav>
As you can see, all the standard binding expressions are available to use with $globals
;
In some of the examples above you can see we can bind to events using a feature called setValue.
setValue does just that, it sets a property value relative to the current binding context or globals if you set it as such.
Here are a couple of scenarios that work on both globals and context properties.
<button click.setValue="$globals.menuVisible = !$globals.menuVisible">Menu</button>
<button click.setValue="title='Hello World'">Set Title</button>
In this case the object being created must be a globally available item.
<button click.setValue="$globals.myDate = new Date()">New Date</button>
Lets say I want to create a class called TestClass and add it to my current context using a property called instance.
In test-class.js you have this code.
export class TestClass {
constructor() {
this.value = "Hello World"
}
}
globalThis.TestClass = TestClass;
Make sure you have this file loaded when you need it.
import "./test-class.js";
Then you can use setValue on a button click to create a new class and set the property.
<button click.setValue="instance = new TestClass()">Create Class</button>
<button click.setValue="$globals.object = {title: ${title}}">Set globals object</button>
The code above will create a object literal and set the "title" property to the current context's title value. This object literal will be set on the globals as a "object" property
You can do some other fun stuff with this, for example, I want to set the mouse location on the mousemove event on a canvas. Normally I would create a event to listen to that and on the event to set the values in code.
Here is a example showing the view model and view that does just that.
<ul click.setValue="selectedId = $event.target.dataset.id">
<template for="item of items">
<li data-id="${item.id}">
<div class="select"></div>
<div>${item.title}</div>
</li>
</template>
</ul>
In the above example you are using a unordered list and each li has a data-id set to a data id.
When you click on the ul, set the property "selectedId" on the binding context data to the data-id value.
This does not set a property on the context but instead sets the data value.
On the context, but it a view model or bindable element, you can be notified of data properties changing by implementing a function using the ${property}Changed
naming convention.
selectedIdChanged(value) {
console.log('selected id changed: ', value);
}
This is not limited to the target property of the event, but you can use any of the event properties.
crsbinding stores it's data values at crsbinding.data
.
This path contains several functions one of them being getValue
.
getValue has two parameters.
- contextId
- propertyPath
If the property path is provided it will fetch you that properties value starting at the context object.
The context object is the root object for the given contextId. If you do not provide the propertyPath it will return the object for you.
$data is a shorthand to the getValue function and you use it the same.
Here is a example of clicking on a UL.
<ul click.setValue="data = $data($event.target.dataset.uid)">
<template for="item of items">
<li data-id="${item.id}" aria-selected.if="$parent.data.id == item.id">
<div class="select"></div>
<div>${item.title}</div>
</li>
</template>
</ul>
You can also use it as part of a binding expression.
<h2>${data.title} - ${$data(0, "menuVisible")}</h2>
In this case we are getting the globals value "menuVisible".
mousedown.setValue="[startPos={x: $event.clientX, y: $event.clientY}; mouseState.isDown = true]"
Indicate that you are performing multiple actions by starting and ending the expression with square brackets ("[", "]").
Each expression is spaparated by a semicolon (";")
import {ViewBase} from "../../src/view/view-base.js";
export default class MouseEvent extends ViewBase {
}
<div>x: ${mouse.x} - y: ${mouse.y}</div>
<canvas mousemove.setValue="mouse={x: $event.x, y: $event.y}"></canvas>
As you move the mouse, the div's content will show the mouses x and y positions. No additional code required.