Studio Developer Manual / Version 2304
Table Of Contents
The class @coremedia/studio-client.client-core/data/ValueExpression
describes
objects that provide access to a possibly mutable value and that notify listeners when the
value changes. They may also allow you to receive a value that can then become the next value of
the expression. Value expressions may be as simple as defining a one-to-one wiring of a
widget property to a model property, but they may encapsulate complex logic that accesses
many objects to determine a result value. As an application developer, you can think of
value expressions as an abstraction layer that hides that potential complexity from you, and
use a common, simple class when wiring up UI state to complex model state.
The Studio SDK offers the following primary implementations of the abstract
ValueExpression
class. You can use the factory methods from
@coremedia/studio-client.client-core/data/ValueExpressionFactory
to create a
ValueExpression
programmatically from TypeScript.
PropertyPathExpression
. This is meant to be used in simple scenarios, where you want to attach a simple bean property to a corresponding widget property. It starts from a bean and navigates through a path of property names to a value. Long paths can be separated with a dot. You can obtain this value expression flavor usingValueExpressionFactory#create(expression, bean)
.FunctionValueExpression
. Use this in scenarios where your UI state requires potentially complex calculations on the model, using multiple beans (remote or local). This value expression object wraps an TypeScript function computing the expression's value. When a listener is attached to the returned value expression, the current value of the expression is cached, and dependencies of the computation are tracked. As soon as a dependency is invalidated, the cached value is invalidated and eventually a change event is sent to all listeners (if the computed value has actually changed). You can useValueExpressionFactory#createFromFunction(AnyFunction, ...args)
to create this flavor. See below for details on how to useFunctionValueExpression
s.
In many cases, you can use the facilities provided by plugins without ever constructing a value expression programmatically. Nevertheless, value expressions are a vital part of the Studio SDK's data binding framework, so it is helpful to understand how they work.
Values
The method getValue()
returns the current value of the expression. How this
value is computed depends on the type of value expression used. Like bean properties, value
expressions may evaluate to any TypeScript value.
When a value expression accesses remote beans that have not yet been loaded, its value is
undefined
. Getting the value or attaching a change listener (see below)
subsequently triggers loading all remote bean necessary to evaluate the expression. If you
need a defined value, you can use the loadValue(AnyFunction)
method instead. The
loadValue
method ensures that all remote beans have been loaded and only then
calls back the given function (and, in contrast to change listeners, only once, see below)
with the concrete value, which is never undefined
.
Like remote beans, value expressions may turn out to be unreadable due to missing read
rights. In this case, getValue()
returns undefined
, too, and the
special condition is signaled by the method isReadable()
returning
false
.
Events
A listener may be attached to a value expression using the method
addChangeListener(listener)
and removed using the method
removeChangeListener(listener)
. The listener must be a function that takes the
value expression as its single argument. The listener may then query the value expression
for the current value.
Contrary to bean events, value expression events are sent asynchronously after the calls modifying the value have already completed. The framework does however not guarantee that listeners are notified on all changes of the value. When the value is updated many times in quick succession, some intermediate values might not be visible to the listener.
The listener is also notified when the readability of the value changes.
As long as you have a listener attached to a value expression, the value expression may in turn be registered as a listener at other objects. To make sure that the value expression can be garbage collected, you must eventually remove all listeners added to it.
A common pattern when adding a listener to a value expression involves an upfront initialization and subsequent updates on events:
import { bind } from "@jangaroo/runtime"; import Config from "@jangaroo/runtime/Config"; import Panel from "@jangaroo/ext-ts/panel/Panel"; import ValueExpression from "@coremedia/studio-client.client-core/data/ValueExpression"; import ValueExpressionFactory from "@coremedia/studio-client.client-core/data/ValueExpressionFactory"; class MyComponent extends Panel { #valueExpr: ValueExpression<number>; constructor(config: Config<MyComponent>) { super(config); this.#valueExpr = ValueExpressionFactory.create<number>(/*...*/); this.#valueExpr.addChangeListener(bind(this, this.#valueExprChanged)); this.#valueExprChanged(this.#valueExpr); } protected override onDestroy(): void { this.#valueExpr && this.#valueExpr.removeChangeListener(bind(this, this.#valueExprChanged)); super.onDestroy(); } #valueExprChanged(valueExpr: ValueExpression<number>): void { const value:number | undefined = valueExpr.getValue(); //... } } export default MyComponent;
Example 5.22. Adding a listener and initializing
By calling the private function once immediately after adding the listener, it is possible to reuse the functionality of the listener for initializing the component. By removing the listener on destroy, memory leaks due to spurious listeners are avoided.
Property Path Expressions
The most commonly used value expression is the property path
expression. It allows you to navigate from an object to a value by successively
reading property values on which the next read operation takes place. For example, a
property path expression may operate on the object obj
and be configured to
read the properties a
, b
, and then c
. If the property
a
of obj
is obj1
, the property b
of
obj1
is obj2
, and the property c
of obj2
is 4, then the expression will evaluate to 4. A path of property names is denoted by a
string that joins the property names with dots, in this case "a.b.c"
. If you
want to address array elements you have to add the index of the element with another dot,
such as a.b.c.3
, and not use the more obvious but false a.b.c[3]
notation.
You can create a property path expression manually in the following way:
import ValueExpression from "@coremedia/studio-client.client-core/data/ValueExpression"; import ValueExpressionFactory from "@coremedia/studio-client.client-core/data/ValueExpressionFactory"; //... const ppe: ValueExpression = ValueExpressionFactory.create("a.b.c", obj);
Example 5.23. Creating a property path expression
The dot notation above might suggest that property path expressions operate exactly like TypeScript expressions, but that is not quite correct. Property path expressions support the following access methods for properties:
read the property of a bean using the
get(property)
method;call a publicly defined getter method whose name consists of the string "get" followed the name of the property, first letter capitalized;
call a publicly defined getter method whose name consists of the string "is" followed the name of the property, first letter capitalized;
read from a publicly defined field of an object. This is the classic TypeScript case.
At different steps in the property path, different access methods may be used.
Even if there are many properties in the path, changes to any of the objects traversed while computing the value will trigger a recomputation of the expression value and potentially, if the value has changed, an event. This is only possible, however, for objects that can send property change events.
For beans, a listener is registered using
addPropertyChangeListener()
.For components using
@jangaroo/ext-ts/mixin/Observable
, a listener is registered usingaddListener()
.
Property path expressions may be updated. When invoking setValue(value)
, a new
value for the value expression is established. This will only work if the last property in
the property path is writable for the object computed by the prefix of the path. More
precisely, a value may be
written into a property of a bean using the
set(property,value)
method;passed to a publicly defined setter method that takes the new value as its single argument and whose name consists of the string "set" followed by the name of the property, first letter capitalized;
written into a publicly defined field of an TypeScript class.
At various points of the API, a value expression is provided to allow a component to bind to
varying data. Using the method extendBy(extensionPath)
adds further property
dereferencing steps to the existing expression. For example,
ValueExpressionFactory.create("a.b.c", obj)
is equivalent to
ValueExpressionFactory.create("a", obj).extendBy("b.c")
.
Function Value Expressions
Function value expressions differ from property path expressions in that they allow arbitrary TypeScript code to be executed while computing their values. This flexibility comes at a cost, however: such an expression cannot be used to update variables, only to compute values. They are therefore most useful to compute complex GUI state that is displayed later on.
To create a function value expression, use the method createFromFunction
of
the class ValueExpressionFactory
.
ValueExpressionFactory.createFromFunction(() => { return ...; });
Example 5.24. Creating a function value expression
The function in the previous example did not take arguments. In this case, it can still use all variables in its scope as starting point for its computation or it might access global variables. To make the code more readable, you might want to define a named function in your TypeScript class and use that function when building the expression.
import ValueExpression from "@coremedia/studio-client.client-core/data/ValueExpression"; import ValueExpressionFactory from "@coremedia/studio-client.client-core/data/ValueExpressionFactory"; class MyClass { //... getExpr(): ValueExpression<number> { return ValueExpressionFactory.createFromFunction(calculateSomething); function calculateSomething(): number { return 42; // calculate some number with dependency tracking } } }
Example 5.25. Creating a value expression from a private function
If you want to pass arguments to the function, you can provide them as additional argument of the factory method. The following code fragment uses this feature to pass a model bean to a static function.
import ValueExpression from "@coremedia/studio-client.client-core/data/ValueExpression"; import ValueExpressionFactory from "@coremedia/studio-client.client-core/data/ValueExpressionFactory"; class MyClass { //... getExpr(): ValueExpression<number> { return ValueExpressionFactory.createFromFunction(MyClass.#calculateSomething); } static #calculateSomething(): number { return 42; // calculate some number with dependency tracking } }
Example 5.26. Creating a value expression from a static function
Function value expressions fire value change events when their value changes. To this end, they track their dependencies on various objects when their value is computed. For accessed beans and value expressions, the dependency is taken into account automatically: whenever the bean or the value expression changes, the value of the function value expression changes automatically, and an event for the function value expression is fired.
If you access other mutable objects, you should make sure that these objects inherit from
Observable
, so that you can register the dependencies yourself. To this end,
you can use the static methods of the class ObservableUtil
. In particular,
the method dependOn(Observable,String)
provides a way to specify the
observable and the event name that indicates a relevant change. As a shortcut, the method
dependOnFieldValue(Field)
allows you to depend on the value of an input field.
import ObservableUtil from "@coremedia/studio-client.ext.ui-components/util/ObservableUtil"; import Observable from "@jangaroo/ext-ts/mixin/Observable"; import BaseField from "@jangaroo/ext-ts/form/field/Base"; class MyClass { #observable: Observable; #field: BaseField; #calculateSomething(): number { ObservableUtil.dependOn(this.#observable, "fooEvent"); ObservableUtil.dependOnFieldValue(this.#field); //... this.#observable.fooMethod(); //... return this.#field.getValue() as number; } }
Example 5.27. Manual dependency tracking
If you register a dependency while no function value is being computed, the call to
ObservableUtil
is ignored. This means that you can register dependencies in
your own functions, and the methods will work whether they are called in the context of a
function value expression or not.
The following listing contains a comprehensive example of a function value expression with detailed code comments concerning where and why dependency tracking is active or not. In the function, a list of titles is gathered from different sources. For each of the titles, a panel is searched and its height is put into a map. This map is the return value of the function.
import {bind} from "@jangaroo/runtime"; import ValueExpression from "@coremedia/studio-client.ext.client-core/data/ValueExpression"; import ValueExpressionFactory from "@coremedia/studio-client.client-core/data/ValueExpressionFactory"; import RemoteBeanUtil from "@coremedia/studio-client.client-core/data/RemoteBeanUtil"; import Content from "@coremedia/studio-client.cap-rest-client/content/Content"; import ObservableUtil from "@coremedia/studio-client.ext.ui-components/util/ObservableUtil"; class MyClass { //... #listenToChanges(): void { const firstContent: Content = this.#getFirstContentValueExpression().getValue(); const secondContentVE: ValueExpression = this.#getSecondContentValueExpression(); const panelHeightsVE: ValueExpression = ValueExpressionFactory.createFromFunction( bind(this, this.#getPanelHeights), firstContent, secondContentVE); } // First content is directly passed to the function. // => No dependency tracking for changes to this.#getFirstContentValueExpression(). // Second content is accessed via ValueExpression. // => Dependency tracking for changes to this.#getSecondContentValueExpression(). #getPanelHeights( firstContent: Content, secondContentVE: ValueExpression): Object { // 'additionalTitles' is just a class field. // => No dependency tracking for changes to its value. let titles = this.additionalTitles || []; // Accessing a Bean property. // => Dependency tracking for changes to the bean property. // Normal beans as opposed to the RemoteBeans below are not asynchronous, // so we do not need to wait until they are loaded. const model = this.getModel(); titles = titles.concat(model.get('additionalTitles') || []); // Contents are of type Bean (RemoteBean). // RemoteBeanUtil.isAccessible() checks if loaded and readable. // If not: // (1) A 'load' call is automatically triggered. // (2) A dependency for a Bean state change is registered. // => dependency tracking for the content beans being loaded. switch (RemoteBeanUtil.isAccessible(firstContent)) { case undefined: // Not loaded yet. // => Interrupt computation. Wait for firstContent being loaded. return undefined; case true: // Loaded and unreadable. // => Abort return null; // Otherwise: RemoteBean loaded, just continue ... } // Dependency tracking for changes to secondContentVE. const secondContent = secondContentVE.getValue(); if (!secondContent) { // Interrupt computation. // Wait for secondContentVE holding a content. return undefined; } // See above: Wait for secondContent being loaded. switch (RemoteBeanUtil.isAccessible(secondContent)) { case undefined: return undefined; case true: return null; } // From here on, both contents are loaded // Their properties can be accessed. // Properties of contents are SubBeans => no need to wait // for them being loaded. let properties = firstContent.getProperties(); titles.push(properties.get("title")); properties = secondContent.getProperties(); titles.push(properties.get("title")); const panelHeights: Object = {}; // For all gathered titles, find a panel with the corresponding title // and get its height. var panelsParentContainer = this.#getPanelsParentContainer(); let addDependencyAdded: Boolean = false; for (let i = 0; i < titles.length; i++) { const title = titles[i]; const panel = panelsParentContainer.getPanelWithTitle(title); if (!panel) { // Panel with title does not exist yet. // Dependency tracking for new childs being added to the container. // 'add' is a component Event of Ext.container.Container if (!addDependencyAdded) { ObservableUtil.dependOn(panelsParentContainer, "add"); // Only add one dependency for 'add'. addDependencyAdded = true; } // Continue with next title. continue; } if (panel.rendered) { // If panel is rendered, just get its height. panelHeights[panel.getId()] = panel.getHeight(); } else { // If panel is not rendered: // => Dependency tracking for the panel being rendered. // 'afterrender' is component event of Ext.Component ObservableUtil.dependOn(panel, "afterrender"); } } // Alternative: // According to the code above, also partial values for // 'panelHeights' are computed: Not found or not rendered // panels are just skipped. Alternatively, we could wait // until all panels are present and rendered. In that case // we need to return 'undefined' each time we encounter // a missing part. It really depends on what 'panelHeightsVE' // is supposed to deliver. return panelHeights; } }
Example 5.28. Comprehensive example of a FunctionValueExpression