2.9.16
Coherent GT
A modern user interface library for games
Data Binding

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

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.

An example

Create a named model

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 player
void CoherentBind(Coherent::UIGT::Binder* binder, Player* player)
{
if (auto type = binder->RegisterType("Player", player))
{
type.Property("leftPos", &Player::LeftPos)
}
}
void RegisterMyModel()
{
// Expose g_Player with the name "g_TestPlayer" to JS
g_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.

Using the model from JS

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.

Update the model

// 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.

Changing nested array and updating the model

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();
}
Warning
If there is a nested array which has a new size it must be removed with RemoveExposedArray. It is because there are cached resources and RemoveExposedArray would force those cache to be cleared.

Data binding syntax overview

HTML/JavaScript data binding syntax

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.

Note
Expressions referring to a single model property are the fastest performance-wise since the bound value is directly assigned within the SDK. Complex expressions require JavaScript evaluation and are more expensive than single value bindings.

Manually binding models

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 DOM
var display = document.createElement('div'),
mana = document.createElement('span');
// Set the data-bind attributes. Note the usage of this
mana.setAttribute('data-bind-value', '{{this.Mana}}');
mana.setAttribute('data-bind-class-toggle', 'red:{{this.Mana}} < 50');
display.appendChild(mana);
// Attach the model
engine.attachToModel(display, g_TestPlayer);
// Add to the DOM
document.body.appendChild(display);
}

Supported data-bind-* attributes

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>
Note
If your text contains new lines 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.

Styling attributes

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.

  • The first one takes a string in the format 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.
  • The second one takes a string in the format 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}}

Note
In the rare case of having multiple data-bind-class expression that can be evaluated to the same value the behaviour will be undefined. Example for that would be : data-bind-class="type + {{player.name}};type + {{game.name}} - if player.name and game.name have the same value this would result in the same class.

Structural data-binding

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.
Warning
More than one structural attribute per element is not allowed and those kinds of elements are ignored by the data bind system

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}}"

Warning
The 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)
{
if (auto type = binder->RegisterType("PetInfo", petInfo))
{
type.Property("speed", &PetInfo::Speed)
.Property("isMount", &PetInfo::IsMount);
}
}
void CoherentBind(Coherent::UIGT::Binder* binder, Pet* pet)
{
if (auto type = binder->RegisterType("Pet", pet))
{
type.Property("name", &Pet::Name)
.Property("info", &Pet::Info);
}
}
void CoherentBind(Coherent::UIGT::Binder* binder, Player* p)
{
if (auto type = binder->RegisterType("Player", p))
{
type.Property("name", &Player::Name)
.Property("health", &Player::GetHealth, &Player::SetHealth)
.Property("pets", &Player::Pets)
.Property("team", &Player::Team);
}
}
// Register the model
Player 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 there
but 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 team
is 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>
Warning
Structural data-binding evaluators generate helper nodes that serve as "anchoring" points in the DOM tree. Modifying those nodes manually will result in undefined behaviour!

Data-binding events

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 pressed
  • keypress: ANY key except Shift, Fn, CapsLock is in pressed position. (Fired continuously.)
  • keyup: ANY key is released

C++ model details

Creating a nameless model

Naming 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>

Updating a model

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 prop
PropertyHandle hpHandle = handle.GetPropertyHandle("health");
player->Health = 42;
// Tell GT that only the health has changed
hpHandle.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 PropertyHandles 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.

Dynamically adding models

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.

Warning
Static DOM nodes (written in markup, and not dynamically created) record the names of the models that affect their appearance when the node is attached to DOM tree (basically after the HTML is parsed). If you register a model with a name that is used in one of those nodes, the binding will be picked up from the record. If you try to unregister the named model and then register it again, the binding won't work any more, since there is no new record about that binding (it's only updated on DOM node attach event).

Delayed model registration

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.

class MyViewListener : public Coherent::UIGT::ViewListener
{
Player* m_Player;
...
virtual void OnBindingModelCreated(
const char* name,
ModelHandle& handle) override
{
if (strcmp(name, "ExpectedModelName") == 0)
{
PropertyHandle hpHandle = handle.GetPropertyHandle("health");
player->Health = 42;
hpHandle.SetNeedsUpdate();
m_View->SynchronizeModels();
}
}
...
};

Unregistering a model

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.