Coherent GT allows for easy communication between your application and the UI via several mechanisms - one of them being data binding. See Binding for C++ for another one.
Data binding synchronizes the state of your UI with that of the game, effectively eliminating a big chunk of JS that would otherwise be needed. Data binding is a pretty popular feature in the web world and if you are familiar with angular.js or react.js this document will be very familiar. However, our data bindings are written in C++ and directly integrated into GT which means that the system has better performance than any other JS library.
As this feature is rather complex let's start with a high-level example to make sense of it all.
To start, you need to expose an object (by reference), to the UI. We'll call the exposed C++ object a "model" as it will serve as the backing store of the data.
You can create a named model using the Coherent::UIGT::View::CreateModel method:
struct Player{int LeftPos;};Player g_Player{ 123 };// Tell GT how to bind the playervoid CoherentBind(Coherent::UIGT::Binder* binder, Player* player){{type.Property("leftPos", &Player::LeftPos)}}void RegisterMyModel(){// Expose g_Player with the name "g_TestPlayer" to JSg_PlayerModelHandle = m_View->CreateModel("g_TestPlayer", &g_Player);}
The View::CreateModel
invocation registers a model named g_TestPlayer
which corresponds to the g_Player
C++ instance. The CoherentBind
above it takes care of exporting the player class just like it would with standard binding.
Once the object is exported from C++, it can be attached to the DOM using the set of data-bind-*
properties. To complete the example from above, imagine that we want to move a nameplate in the UI using the player's left position:
<div class="nameplate" data-bind-style-left="{{g_TestPlayer.leftPos}}"></div>
Note the special double curly brace syntax ({{expression}}
) that you need to use.
With the model exposed and the HTML using it, the <div>
element is now linked to the player. We aren't done yet though - to save on computation GT will only update the element if you tell it to do so.
// Somewhere in your game loop void Update() { ... // Either update via the model handle: g_PlayerModelHandle->SetNeedsUpdate(); // Or through the model itself view->UpdateWholeModel(g_Player); // Either of the above is enough; don't call them both // Finally, tell GT that now is a good time to synchronize all changes view->SynchronizeModels(); }
With these 3 simple steps the <div>
will automatically update anytime the model changes. Note that you didn't have to write any JS to syncronize the UI and the game. Although this example is contrived, you can imagine how powerful this feature can become when dealing with a complex screen powered by tens of variables. Scroll below for more details on how to make use of the subsystem.
struct Cities { std::vector<std::string> m_Streets; size_t m_LastUpdatedSizes = 0; }; struct Country { Cities m_Cities; } g_Country; void CoherentBind(Coherent::UIGT::Binder* binder, Cities* cities) { // ....... } void CoherentBind(Coherent::UIGT::Binder* binder, Country* country) { // ....... } void RegisterMyModel() { g_CountryModelHandle = m_View->CreateModel("g_TestCountry", &g_Country); } void Update() { if(g_Country.m_Cities.m_LastUpdatedSizes != g_Country.m_Cities.m_Streets.size()) { view->RemoveExposedArray(&g_Country.m_Cities.m_Streets[0]); g_Country.m_Cities.m_LastUpdatedSizes = g_Country.m_Cities.m_Streets.size(); } view->UpdateWholeModel(&g_Country); view->SynchronizeModels(); }
RemoveExposedArray
. It is because there are cached resources and RemoveExposedArray
would force those cache to be cleared.The syntax of the data binding attributes is a JavaScript expression. The simplest expressions only refer to the model's properties and are encapsulated in double curly braces like we just saw above:
<div data-bind-style-left="{{myModel.leftPos}}"></div>
where myModel
is a named model which has a leftPos
property.
You can also construct complex expressions such as
<div data-bind-style-left="{{myModel.LeftPos}}.toFixed()"></div><div data-bind-style-left="Math.pow({{myModel.LeftPos}}, 2)"></div>
Note that only the code refering to the model's properties is inside the curly braces.
A node can also be attached to a model using the engine.attachToModel(DOMNode, exposedModel)
JavaScript method (provided from coherent.js). When manually attaching the model, use this
to refer to the model inside the data bindings attributes:
engine.on('Ready', function () {// Manually create the DOMvar display = document.createElement('div'),mana = document.createElement('span');// Set the data-bind attributes. Note the usage of thismana.setAttribute('data-bind-value', '{{this.Mana}}');mana.setAttribute('data-bind-class-toggle', 'red:{{this.Mana}} < 50');display.appendChild(mana);// Attach the modelengine.attachToModel(display, g_TestPlayer);// Add to the DOMdocument.body.appendChild(display);}
data-bind-value
The data-bind-value
attribute takes a value and assigns the node's textContent
property to it.
Example:
<span data-bind-value="{{player.health}}"></span>
If you want to round the value then you can use the toFixed
method.
Example:
<span data-bind-value="{{player.health}}.toFixed(2)"></span>
data-bind-value
will require additional setup since by default line endings are ignored when parsing HTML. If you want to display them it is required to add the white-space: pre;
CSS property which will cause the line ending symbols to break lines.The following attributes allow you to modify the element's style:
Data bind attribute | Affected style property | Acceptable values |
---|---|---|
data-bind-style-left | left | string or number |
data-bind-style-top | top | string or number |
data-bind-style-opacity | opacity | floating point number between 0 and 1 |
data-bind-style-width | width | string or number |
data-bind-style-height | height | string or number |
data-bind-style-color | color | CSS color as string |
data-bind-style-background-color | background-color | CSS color as string |
data-bind-style-background-image-url | background-image | url as a string |
data-bind-style-transform2d | transform | comma separated list with 6 numbers as a string |
All the properties above that take numbers will assume that the number is a measurement in pixels (e.g. binding 42 to data-bind-style-left
will be equivalent to left: 42px
).
There're two more styling attributes - data-bind-class-toggle
and data-bind-class
.
class-name:bool_condition[;class-name:bool_condition]
. The class-name is the name of some CSS class and bool_condition
is an expression that evaluates to a boolean. If the condition evaluates to true
, the class specified by class-name is added to the element, otherwise it is removed.class-name[;class-name]
. The class name is the CSS class. The class specified by class-name is added to the element.Let's see an example with class toggling:
<head><style>.red {background-color: red;}</style></head><body><div data-bind-class-toggle="red:{{this.Health}} < 50">Something red</div>
The "red" class will be present on the element as long as {{this.Health}} < 50
is true
, changing the element's background to red. Otherwise it won't be applied and the element will have whatever background it usually has.
Let's see an example with class :
<head><style>.class-type-left-10 {left : 10px;}.class-type-left-20 {left : 20px;}.class-type-top-30 {top : 30px;}.class-type-top-40 {top : 40px;}</style></head><body><div data-bind-class="'class-type-'+{{this.type_1}};'class-type-'+{{this.type_2}}"></div>
The element will be moved on different places depends on {{this.type_1}}
and {{this.type_2}}
The attributes above are useful for modifiying single DOM nodes but the real power of the data binding system stems from the fact that you can also modify the entire DOM tree with it.
The following attributes allow for structural changes to the DOM:
data-bind-if
: Displays a DOM element based on a condition. The expressions in the attribute value should evaluate to a boolean value.data-bind-switch
: Goes through its children, looking for data-bind-switch-when="constant"
and only displays this child whose switch-when
attribute equals the value of the switch
expression. Does nothing if no such child exists. Optionally, a data-bind-switch-default
attribute can be specified, which will be displayed should no children match the rule.data-bind-for
: Repeats the DOM node for each element in the collection. The syntax is data-bind-for="iter:{{myModel.arrayProperty}}"
, where myModel.arrayProperty
is an array property of myModel
, and iter
is a variable used for iteration, which is available in child DOM node evaluators.You can also use the full form data-bind-for="index, iter:{{myModel.arrayProperty}}"
, where index
is loop counter. If you don't need use the index or iterator you can use _
e.g. data-bind-for="index, _:{{myModel.arrayProperty}}"
data-bind-for
attribute currently supports iterating over arrays bound by-ref only! Any other type of collection won't work.Following are a few examples for the structural constructs.
Start with the model that drives the examples below:
struct PetInfo{int Speed;bool IsMount;PetInfo(int speed, bool isMount): Speed(speed), IsMount(isMount){};};struct Pet{std::string Name;PetInfo Info;Pet(const std::string name, PetInfo info): Name(name), Info(info){};};struct Player{std::string Name = "";float Health = 100;std::string Team = "red";std::vector<Pet> Pets ={Pet("Sabretooth tiger", PetInfo(80, true)),Pet("Eagle", PetInfo(90, false)),Pet("Pony", PetInfo(70, true))};float GetHealth() const { return Health; }void SetHealth(float health) { Health = health; }};void CoherentBind(Coherent::UIGT::Binder* binder, PetInfo* petInfo){{type.Property("speed", &PetInfo::Speed).Property("isMount", &PetInfo::IsMount);}}void CoherentBind(Coherent::UIGT::Binder* binder, Pet* pet){{type.Property("name", &Pet::Name).Property("info", &Pet::Info);}}void CoherentBind(Coherent::UIGT::Binder* binder, Player* p){{type.Property("name", &Player::Name).Property("health", &Player::GetHealth, &Player::SetHealth).Property("pets", &Player::Pets).Property("team", &Player::Team);}}// Register the modelPlayer player;view->CreateModel("g_Player", &player);
Try a data-bind-if
:
< !-- displays a warning message if the player's health is low --><div data-bind-if="{{g_Player.health}} < 40" id="playerHPLowWarn">Player needs a medic!</div>< !-- If g_Player.health is less than 40 will result in: --><div data-bind-if="{{g_Player.health}} < 40" id="playerHPLowWarn">Player needs a medic!</div>< !-- If the player's health is >= 40, it will result in an inactive node - the element will be therebut it will be hidden from view. -->
data-bind-switch
also works as you'd expect it to:
< !-- Displays the team of the player.If g_Player.team is something other than "red", "blue" or "green" nothing will be displayed--><div data-bind-switch="{{g_Player.team}}"><div data-bind-switch-when="red" class="teamDesc">Team red</div><div data-bind-switch-when="green" class="teamDesc">Team green</div><div data-bind-switch-when="blue" class="teamDesc">Team blue</div></div>< !-- Assuming g_Player.team == 'red', the switch will result in --><div data-bind-switch="{{g_Player.team}}"><div class="teamDesc">Team red</div></div>< !-- Displays the team of the player or "No team" if it's something other than"red", "blue" or "green" --><div data-bind-switch="{{g_Player.team}}"><div data-bind-switch-when="red" class="teamDesc">Team red</div><div data-bind-switch-when="green" class="teamDesc">Team green</div><div data-bind-switch-when="blue" class="teamDesc">Team blue</div><div data-bind-switch-default class="teamDesc">No team</div></div>
Finally, we reach the most interesting structural bind - the data-bind-for
:
< !-- Enumerates all pets of the player --><div class="petsList"><div data-bind-for="iter:{{g_Player.pets}}" class="petItem">< !-- Note the double curly braces around the iterator! --><div class="petName" data-bind-value="{{iter}}.name"></div><div class="petSpeed" data-bind-value="{{iter}}.info.speed"></div><div class="petType" data-bind-value="{{iter}}.info.isMount ? 'Mount' : 'Companion'"></div></div></div>< !-- Will result in --><div class="petsList"><div class="petItem"><div class="petName">Sabertooth tiger</div><div class="petSpeed">80</div><div class="petType">Mount</div></div><div class="petItem"><div class="petName">Eagle</div><div class="petSpeed">90</div><div class="petType">Companion</div></div><div class="petItem"><div class="petName">Pony</div><div class="petSpeed">70</div><div class="petType">Mount</div></div></div>< !-- Structural evaluators can be nested -->< !-- enumerates all pets of the player if her health is above 50 and the teamis either "red" or "blue" --><div data-bind-if="{{g_Player.health}} > 50" id="ifTest1"><div data-bind-if="{{g_Player.team}} == 'red' || {{g_Player.team}} == 'blue'"><div class="petsList"><div data-bind-for="iter:{{g_Player.pets}}" class="petItem"><div class="petName" data-bind-value="{{iter}}.name"></div><div class="petSpeed" data-bind-value="{{iter}}.info.speed"></div><div class="petType" data-bind-value="{{iter}}.info.isMount ? 'Mount' : 'Companion'"></div></div></div></div></div>
< !-- Access loop counter --><div class="petsList"><div data-bind-for="index, iter:{{g_Player.pets}}" class="petItem"><div data-bind-class="'petName' + {{index}}" data-bind-value="{{iter.name}}"></div><div></div>< !-- Will result in --><div class="petsList"><div class="petItem"><div class="petName0">Sabertooth tiger</div></div><div class="petItem"><div class="petName1">Eagle</div></div><div class="petItem"><div class="petName2">Pony</div></div></div>
The data-binding events are dom element attributes for attaching event listeners on the DOM.
Example:
<script type="text/html" data-bind-template-name="ComponentName"><div data-bind-for="iter:{{g_Player.weapons}}"><div data-bind-[eventName]="{{component}}.callback(event, {{iter}}, {{this}}, this)">Text</div><div data-bind-[eventName]="{{this}}.callback(event, {{iter}}, {{this}}, this)">Text</div><div data-bind-[eventName]="window.callback(event, {{iter}}, {{this}}, this)">Text</div></div></script>
event
- The JavaScript Event object from the fired event.[eventName]
- All events listed in Supported Events. Example: click, mouseup, dblckick, etc.[callback]
- There would be three different ways of sending callback:{{component}}.callbackName1
- functions added to the Component object: engine.getComponent('componentName').addEvents({callbackName1: function(event, iter, this, domElement) {},callbackName2: function(event, iter, this, domElement) {}});
{{modelName}}.methodName
- These are model methods, no matter if exposed from C++ or added from JavaScript: modelName.method = function(event, iter, this, domElement) {};
globalFunction
- These are functions in the global namespace: window.callbackName = function(event, iter, this, domElement) {};
modelName.property
- This has the same syntax and functionality as all our data-bind attribute values. It would be evaluated the same way. (It can also be a complex expression) The only constraint is that it must be a JS Object. Exactly the way data-bind-model works.this
without {{}} represents the dom element on which the event has ocurred.Supported Events
mouseenter
: A pointing device is moved onto the element that has the listener attached.mouseleave
: A pointing device is moved out of the element that has the listener attached.mouseover
: A pointing device is moved onto the element that has the listener attached or onto one of its children.mousemove
: A pointing device is moved over an element. (Fired continuously as the mouse moves.)mousedown
: A pointing device button is pressed on an element.mouseup
: A pointing device button is released over an element.click
: A pointing device button (ANY button; soon to be primary button only) has been pressed and released on an element.dblclick
: A pointing device button is clicked twice on an element.focus
: An element has received focus. The main difference between this event and focusin
is that only the latter bubbles.blur
: An element has lost focus. The main difference between this event and focusout
is that only the latter bubbles.focusin
: An element is about to receive focus. The main difference between this event and focus
is that the latter doesn't bubble.focusout
: An element is about to lose focus. The main difference between this event and blur
is that the latter doesn't bubble.keydown
: ANY key is pressedkeypress
: ANY key except Shift, Fn, CapsLock is in pressed position. (Fired continuously.)keyup
: ANY key is releasedNaming every model you need to expose from C++ is both annoying and unnecessary. Instead, you can send nameless models and have the JS attach the model to the DOM.
Firstly, send the object from C++ to JavaScript using ByRef bindings:
view->TriggerEvent("attachNamelessModel", Coherent::UIGT::ByRef(player));
Then, in the HTML/JS you handle the event like so:
function instantiateTemplate(selector) {var template = document.querySelector(selector),div = document.createElement('div');div.innerHTML = template.textContent;return div;}engine.on('attachNamelessModel', function (model) {var instance = instantiateTemplate('#player-template');engine.attachToModel(instance, model);// Do something with the DOM node "instance" here}
The engine.attachToModel(DOMNode, exposedModel)
will automatically register a nameless model to attach to. You can obtain a model handle in the C++ code by passing your View and model to the constructor of Coherent::UIGT::ModelHandle.
Finally, assuming that your HTML looks like below, we'll get a nameplate for the player.
<script type="text/html" id="player-template"><div id="attach-player" data-test-class="gm" data-bind-class-toggle="gm:{{this.gameMaster}}"><label>Name: <span id="attach-name" data-test="name" data-bind-value="{{this.name}}">CoolName</span></label><label>Health: <span id="attach-health" data-test="health" data-bind-value="{{this.health}}"></span></label><div id="attach-health-class" data-test-class="good-shape" data-bind-class-toggle="good-shape:{{this.health}} >= 100"><div id="attach-health-bar" data-style="width" data-bind-style-width="{{this.health}} / 1000"></div></div></div></script>
Models can be updated in two different ways - the Coherent::UIGT::View class and the Coherent::UIGT::ModelHandle / Coherent::UIGT::PropertyHandle.
The most basic method for updating is Coherent::UIGT::View::UpdateWholeModel. It will update all properties of the model, including those that haven't actually changed.
More fine-grained control over updating can be obtained through the handle classes.
A Coherent::UIGT::ModelHandle is returned upon invoking Coherent::UIGT::View::CreateModel API or when created manually through its constructor. In case of the latter, you need to ensure the model is already registered, either through View::CreateModel
or through a nameless model from JavaScript.
The ModelHandle
allows you to update the entire model too - just call ModelHandle::SetNeedsUpdate
. To recap:
// Option 1 (through View::CreateModel)ModelHandle handle = m_View->CreateModel("player", m_Player.get();// Option 2 (manually; assuming m_Player has already been registered)ModelHandle handle(m_Player.get(), m_View):handle.SetNeedsUpdate();
The ModelHandle
allows clients to obtain a Coherent::UIGT::PropertyHandle by property name using the Coherent::UIGT::ModelHandle::GetPropertyHandle method. The returned property handle can mark single properties (or specific array indices) as dirty through the Coherent::UIGT::PropertyHandle::SetNeedsUpdate or Coherent::UIGT::PropertyHandle::SetNeedsUpdateArrayIndex methods:
ModelHandle handle = m_View->CreateModel("player", m_Player.get();...// Get property handle to the health propPropertyHandle hpHandle = handle.GetPropertyHandle("health");player->Health = 42;// Tell GT that only the health has changedhpHandle.SetNeedsUpdate();
Using the fine-grained API for updating single properties of models is more verbose, but will have better performance in case the models have a lot of properties, whose values have not changed.
All changes marked through the ModelHandle
and PropertyHandle
s are simply marked as needing update and changes are not propagated immediately. Clients can choose an appropriate moment in time to do the update through View::SynchronizeModels
, which does the actual update.
Attaching a model internally traverses all children of the attached DOM node and takes into account their data-bind-* attributes as well.
Dynamically setting an attribute on an already attached DOM node will have no effect. Data-bind values are currently only registered on the node before attachment.
When running in Asynchrous mode, the model won't be created until some time later after the Coherent::UIGT::View::CreateModel call. Thus when using the CreateModel
method it's important to get your property handles in the Coherent::UIGT::ViewListener::OnBindingModelCreated as the handles may not be ready for usage. This is not required when using sync mode, but it's still a good practice. The next example shows how to use OnBindingModelCreated
.
{Player* m_Player;...const char* name,ModelHandle& handle) override{if (strcmp(name, "ExpectedModelName") == 0){PropertyHandle hpHandle = handle.GetPropertyHandle("health");player->Health = 42;hpHandle.SetNeedsUpdate();m_View->SynchronizeModels();}}...};
Finally, you can unregister models from binding using the Coherent::UIGT::View::UnregisterModel API. This removes the model by instance pointer. Unregistration will not remove any elements bound to the model - they'll preserve their last state.