This section explains and documents the various Javascript API calls for instantiating and working with ChangeAppliers. In practice, users will use the ChangeAppliers which are automatically constructed for every Model Component as its top-level member applier and will not construct their own. Furthermore, a good deal of the use made of ChangeAppliers will take the form of Declarative Configuration rather than literal JavaScript API calls. This page presents both programmatic calls and their declarative equivalents where they exist.
Registering interest in model changes using a ChangeApplier
Declarative style for listening to changes
The declarative style for registering interest in change events uses an entry in the modelListeners
options area of a
modelComponent
. These listeners are attached to the applier during the construction process of the entire component
(and its surrounding tree) and so will therefore become notified as part of the
initial transaction - they will therefore get to observe the model changing
state from its primordial value of undefined to holding their initial resolved value. This is the recommended way of
listening to model changes using the ChangeApplier system.
Each record in the modelListeners block has the format <shortModelPathReference or namespace>: <modelListener declaration>
.
The left and right hand sides of this definition will be explained in the subsequent sections:
Model Path References
A <shortModelPathReference>
has the form:
Syntax definition for <shortModelPathReference> - the key in
modelListeners options block for a modelComponent
|
||
---|---|---|
Syntax | Meaning | Examples |
Simple String | Reference to a model path in this component |
|
IoC Reference | Reference to a model path in another component |
|
The key for a modelListener will be interpreted as a <shortModelPathReference>
if the modelListener declaration does
not contain a member named path
. If the modelListener declaration includes path
, then the key will be interpreted as
a namespace instead, unless the declaration includes a namespace
member, which takes priority as defining the
namespace.
The four examples presented in the "Examples" column are parallel for the two cases - they respectively match changes
occurring in the same parts of the target model, only in the first row they match into the model attached to this
component (the same one in which the modelListeners
record appears) and in the second row they match into the model
attached to another component - one referenced by the Context Expression otherComponent
.
Note that more complex path specifications may be provided in the path
member of a model listener declaration.
Model Listener Declaration
A model listener declaration block has the same form and meaning as any of the record types supported by
Invokers and Listeners - including the one-string compact
syntax documented with Invokers, and the use of Priorities. Like standard
event listener entries, model listeners can provide a namespace
entry. Just one model listener with a particular
namespace will be registered on a particular ChangeApplier.
A model listener declaration block includes three extra features beyond those found in ordinary event listeners. Firstly
is the possibility of including a member path
of type
<modelPathReference>
(a String
or Object
), which can hold a more complex path specification for the model listener
match than can be encoded in the single string in a <shortModelPathReference>
, secondly the ability to filter a change
based on its source, using the members includeSource
and
excludeSource
, and finally the possibility that any IoC-resolved material in the listener declaration may match the
special context name change
which corresponds to the model change that the listener is
reacting to. These entries are described in the linked sections below:
The path entry in a model listener declaration
The path
entry fulfils the same basic function as the <shortModelPathReference>
which may form the key of the
listener declaration, but allows a richer set of path specifications to be used for specifying which changes this model
listener will respond to. Note that if the path
entry is supplied, then the key of the listener will be interpreted as
a namespace for the listener rather than as a path specification.
Possible values for the path member of a model listener declaration (first two rows define
<modelPathReference> )
|
||
---|---|---|
Type | Meaning | Examples |
String |
A <shortModelPathReference> |
See examples in section Model Path References |
Object |
A <modelPathRecord> , including members
|
These examples encode exactly the same path expressions, in the same order as in the section
Model Path References
|
Array |
An array of <modelPathReference> : members are either a String holding a
<shortModelPathReference> or an Object <modelPathRecord> . The
listener will be notified when matching on
multiple paths
|
[
"position", {
segs: ["windowHolders", "{that}.options.ourWindow"]
}
]
|
Matching on multiple paths in a single model listener declaration
When the path
member of a model listener declaration holds an Array
, the listener will be notified when
Note that a listener which specifies references to multiple component targets in such a list will only receive one
notification per component at the end of a transaction where a change matches. For example, if the listener list
contains {otherComponent}.model.x.y
and {otherComponent}.model.x
, the listener will only be notified once for
{otherComponent}
for a matching change. A listener which supplies an array of more than one element in
path
will not be able to make use of either the special context change
or the possibility of using the wildcard character *
in the final path segment. Note that elements of
segs
may themselves consist of IoC references resolving to configuration in the tree (although they may not
hold references to model material - they are evaluated just once when the component constructs).
The special context change
An extra context name is available in a model listener block by the name of change
. This is bound to
the particular change event which triggered this listener. This context behaves as an object with the following fields:
Members of the {change} object bound in a model listener declaration |
||
---|---|---|
Member | Type | Description |
{change}.value |
Any type | The new value which is now held at the model path matched by this model listener block |
{change}.oldValue |
Any type | The previous value which was held at the matched model path, before it was overwritten by the change being listened to |
{change}.path |
String |
The path at which this change occurred. In general this will be the same as the path registered as the
modelPathReference for this block - however it may be one segment longer if a wildcard path
was used (see section on wildcards)
|
Source tracking and filtering in model listener blocks
Each transaction holding one or more changes is associated with a particular source. Model listeners can use two
special directives, excludeSource
and includeSource
in order to register their interest or disinterest in receiving
changes from particular sources. The default behaviour is to receive all changes from all sources. The values of these
fields are single strings representing sources, or arrays of these strings. Three currently supported built-in sources
are init
, relay
and local
- in addition, arbitrary user-defined sources may be attached to a change by making use
of the source
element of a changeRecord
or argument in call to
applier.change
.
Fields of a model listener declaration operating source filtering | ||
---|---|---|
Member | Type | Description |
excludeSource |
String/Array of String |
A source or set of sources for which this listener should not receive notifications |
includeSource |
String/Array of String |
A source or set of sources for which this listener should receive notifications. If
excludeSource is empty, only changes from these sources will be received. If
excludeSource is not empty, these values will take priority.
|
The values of built-in sources supported as values in excludeSource
and includeSource
are as follows:
Values for built-in sources supported as entries in excludeSource and
includeSource as part of a model listener declaration
|
|
---|---|
Source | Description |
init |
The change arising from the initial transaction. During this change, the listener will observe
the value of the model changing from undefined to its consistent initial value, during the
overall process of component construction
|
relay |
A change resulting from model relay from another linked component in the model skeleton, elsewhere in the component tree. |
local |
A change directly triggered via the ChangeApplier on this component - either via a declarative record
holding changePath , or programmatically using an applier.change() call
|
* |
Matches all sources |
Example featuring built-in change source filtering:
fluid.defaults("examples.sourceExample1", {
gradeNames: ["fluid.modelComponent"],
model: {
things: "initial value"
},
modelListeners: {
things: {
funcName: "fluid.log",
excludeSource: "init",
args: ["Value changed to ", "{change}.value"]
}
}
});
var that = examples.sourceExample1(); // no log from this line
that.applier.change("things", "new value"); // logs "Value changed to new value"
This example will not log the transition from the initial model state of undefined
to the console.
It will, however, log the value new value
triggered via the ChangeApplier API.
Example featuring user-defined change source filtering
fluid.defaults("examples.sourceExample2", {
gradeNames: ["fluid.modelComponent"],
model: {
position: 20
},
invokers: {
scrollTo: {
changePath: "position",
value: "{arguments}.0",
source: "scrollbar"
}
},
modelListeners: {
position: {
funcName: "fluid.log",
excludeSource: "scrollbar",
args: ["Value changed to ", "{change}.value"]
}
}
});
var that = examples.sourceExample2(); // This will log the initial change to value 20
that.scrollTo(30); // This logs nothing - source "scrollbar" is excluded
that.applier.change("position", 40); // This will log "Value changed to 40"
This example will log the transition from the initial model state of undefined
to the console, since
unlike sourceExample1
it does not have excludeSource: init
. However, changes caused to the model
via the invoker scrollTo
will not be logged since they have the source scrollbar
marked
to them. The third interactive line shows that changes to the model without any user source
marking will be logged by the listener.
Warning and workaround for issues involving initial transaction and onCreate
race issues
Note: The current implementation of the ChangeApplier has a bug
(FLUID-5519) which will often cause a model listener to be notified
before much of the surrounding component has constructed. This can be annoying, since the model listener may want to
rely on other infrastructure (e.g. invokers, etc.) that it cannot be sure have been constructed. For this reason,
excludeSource: "init"
is a useful way of stabilising this behaviour until the implementation is fixed (fix will be
delivered as part of FLUID-4925).
Wildcards in model path references
The last path segment of a model path reference may be "*"
. Whether the reference has this "*"
suffix or not, the
reference matches exactly the same set of changes - the only difference is in how they are reported. A path reference
of "things"
will match all changes occurring below this path segment, and report all those occurring within a single
transaction as a single change. A path reference of "things.*"
will match the same changes, but will report one change
for each immediately nested path segment touched by the changes. For example, the following definition will log just one
fluid.defaults("examples.pathExample1", {
gradeNames: ["fluid.modelComponent"],
modelListeners: {
things: {
funcName: "fluid.log",
args: ["{change}.value", "{change}.path"]
}
}
});
var that = examples.pathExample1();
that.applier.change("things", {a: 1, b: 2});
// this logs {a: 1, b: 2}, "things" to the console
However, the following example which just differs in the listener path (swapping "things"
for "things.*"
) will log
two changes:
fluid.defaults("examples.pathExample2", {
gradeNames: ["fluid.modelComponent"],
modelListeners: {
"things.*": {
funcName: "fluid.log",
args: ["{change}.value", "{change}.path"]
}
}
});
var that = examples.pathExample2();
that.applier.change("things", {a: 1, b: 2}); // logs 2 lines
// Line 1: 1, "things.a"
// Line 2: 2, "things.b"
The standard way to be notified of any changes to the model in a single notification is to use a model path reference
consisting of the empty string ""
. Use of "*"
will react to the same changes, but will report multiple notifications
for compound modifications as in the above example.
It is not currently possible to supply more than one wildcard segment per path reference, or to supply the wildcard at any position in the string other than as the last path segment.
Programmatic style for listening to changes
The programmatic style for registering interest in model changes uses an API exposed by the ChangeApplier on its member
modelChanged
that is very similar to that exposed by a standard Infusion Event - the
difference is that the addListener
method accepts an extra 1st argument, spec
- an Object
which holds the same
model path reference in path
or segs
documented in the previous section on declarative binding:
applier.modelChanged.addListener(spec, listener, namespace);
applier.modelChanged.removeListener(listener);
spec
may also include the standard member priority
seen in the declarative record.
Note: This style of listening to changes is discouraged, but may be the right choice in some applications. For example - the listener to be attached may not be available at the time the component is constructed. Note that programmatically attached listeners will miss observation of the initial transaction as well as any other model changes that have occurred up to the point where they are registered.
The listener is notified after the change (or set of coordinated changes forming a transaction) has already been applied to the model. The signature for these listeners is
function listener(value, oldValue, pathSegs, changeRequest, transaction) {
// ...
}
Parameter | Description |
---|---|
value |
The new (current) model value held at the path for which this listener registered interest |
oldValue |
A "snapshot" of the previous model value held at that path |
pathSegs |
An array of String path segments holding the path at which value and
oldValue are/were held
|
changeRequest |
May contain a single ChangeRequest
object which was responsible for this change, but will often be empty. This signature element is not a
stable API
|
transaction |
May contain a Transaction object which this
change was bound to. Primarily useful for the source member which can be used to manually
check which change sources the transaction is marked to. This signature element is not a stable API
|
Users will in most cases only be interested in the first argument in this signature.
Triggering a change using a ChangeApplier
Declarative style for triggering a change
The declarative style for triggering model changes involves an IoC record (a change record) that is supported in
various places in component configuration, in particular as part of the definition of both Invokers and
Listeners of an IoC-configured component. This style of record is recognised by its use of
the special member changePath
(a "duck typing field") which determines
which path in which component model will receive the change.
changeRecord for firing changes by declarative binding |
||
---|---|---|
Member | Type | Description |
changePath |
<modelPathReference> {String|Object} |
The reference to the model path in a model somewhere in the component tree where the change is to be
triggered. This has the same syntax as the model path references documented above for declarative
listening, only wildcard forms are not supported. Four examples:
|
value |
Any type |
The value which should be stored at the path referenced by changePath . If this contains
compound objects (built with {} ), these will be merged into the existing values in the
model. If this contains arrays (built with [] ) these will overwrite existing values at that
path.
|
type |
String (optional) |
If this holds the value DELETE , this change will remove the value held at
changePath . In this case, value should not be supplied. This is the
recommended way of removing material from a model - it has the effect of the delete
primitive of the JavaScript language. Sending changes holding a value of null
or undefined does not have the same effect, as per the JavaScript language spec.
|
source |
String/Array of String/Object (optional) |
Any string or strings supplied here will be marked to the change as it propagates. Model listeners and
relay rules can then choose to opt in or opt out of responding to this change by means of the
source-related
includeSource and excludeSource members in their records. If an
Object is supplied here, it is assumed that the sources are encoded in its keys, and its
values will be ignored.
|
Example of declarative triggering of changes
In the below example, we construct an invoker that will set the entire model of the current component to whatever value
is supplied as its first argument - this is achieved by giving its record a changePath
of ""
and binding its value
to {arguments}.0
:
fluid.defaults("examples.changeExample", {
gradeNames: ["fluid.modelComponent"],
model: "initialValue",
invokers: {
changer: {
changePath: "",
value: "{arguments}.0"
}
}
});
var that = examples.changeExample();
that.changer("finalValue");
console.log(that.model); // "finalValue"
Programmatic style for triggering a change
There are two calls which can be used to fire a change request - one informal, using immediate arguments, and a more
formal method which constructs a concrete changeRequest
object.
applier.change(path, value, type, source);
Fields in a changeRequest object, or (in order) arguments to applier.change
|
||
---|---|---|
Path | Type | Description |
path |
String|Array of String |
An EL path into the model where the change is to occur, expressed either as a single string or an array of path segments |
value |
Any type | An object which is to be added into the model |
type |
(optional) "ADD" or "DELETE" |
A key string indicating whether this is an ADD request (the default) or a
DELETE request (a request to unlink a part of the model)
|
source |
(optional) String|Array of String|Object
|
One or more strings representing source s which should be marked to this change. See
documentation on the source member
of a changeRecord
|
The semantics and values are exactly the same as described in the section on declarative triggering above - with the
difference that IoC references may not be supplied for path
.
applier.fireChangeRequest(changeRequest);
where a changeRequest
is an object holding the above named parameters in named fields - e.g.
{path: "modelPath", value: "newValue"}
. Note that a changeRequest
is the same as a
changeRecord
only the path is encoded in a field named path
rather
than changePath
.
change
and fireChangeRequest
reach exactly the same implementation - the only difference is in the packaging of the
arguments. For change
they are spread out as a sequence of 4 arguments, whereas for fireChangeRequest
, they are
packaged up as named fields (path
, value
and type
) of a plain JavaScript object. Such an object is called a
changeRequest and is a convenient package for these requests to pass around in an event pipeline within the
framework.
The programmatic style for firing changes is less strongly discouraged than the programmatic style for listening to changes is - since it does not run into the same lifecycle issues that programmatic listeners do. However, the declarative style for triggering changes should be used wherever it can.
Example of two styles of declarative model listener registration
Users can freely define very fine or coarse-grained listeners for changes in a model using the ChangeApplier. Here are some examples using the declarative model listener registration syntax:
fluid.defaults("my.component", {
gradeNames: "fluid.modelComponent",
invokers: {
printChange: {
"this": "console",
method: "log",
args: ["{arguments}.0"]
}
},
model: {
cats: {
hugo: {
name: "Hugo",
colours: ["white", "brown spots"]
},
clovis: {
name: "THE CATTT!",
colours: ["white", "black spots", "black moustache"]
}
}
},
modelListeners: {
// Will fire individual change events whenever any part of "cats.hugo" changes.
// {change}.value will correspond to each changed path within "hugo".
"cats.hugo.*": {
funcName: "{that}.printChange",
args: ["{change}.value"]
},
// Will fire a single composite change event whenever any part of "cats.clovis" changes.
// {change}.value will contain the new state of the "clovis" object.
"cats.clovis": {
funcName: "{that}.printChange",
args: ["{change}.value"]
}
}
});
// Example usage.
var c = my.component();
c.applier.change("cats.hugo", {
name: "Hugonaut",
colours: ["hard to tell"]
});
// "Hugonaut"
// ["hard to tell"]
c.applier.change("cats.clovis.name", "THER CATTT!");
// {name: "THER CATTT!", colours: ["white", "black spots", "black moustache"]}
Low-level ChangeApplier APIs
These are not recommended for typical users of the framework, and are not stable.
Instantiating a ChangeApplier
Instantiating a ChangeApplier manually is not recommended in current versions of the framework. Its implementation is tightly bound into its location in an IoC component tree and should be constructed by the IoC system itself.
Operating transactions manually
A user may be interested in economising on notifications to model updates; by batching these up into a single
transaction, there will just be a single notification of each listener which is impacted around the model skeleton. This
facility is not a stable API (at the Infusion 2.0 version level and before); however, its use can't be strongly
discouraged since it is the only way of avoiding certain unwanted model notifications, especially for sequences of
changes which include a DELETE
.
A transaction can be opened using the initiate()
method of the applier function which returns a transaction object:
var myApplier = myModelComponent.applier;
var myTransaction = myApplier.initiate();
The transaction object exposes an API which agrees with the ChangeApplier's own API for triggering changes, which can be used to trigger changes within the transaction:
myTransaction.fireChangeRequest(requestSpec1);
myTransaction.fireChangeRequest(requestSpec2);
// ...
The transaction can be completed using the commit()
function of the transaction object:
myTransaction.commit();
A single modelChanged
event will be fired on completion of the commit, regardless of the number of change requests.