ChangeApplier API

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
  • "modelPath"
  • "modelPath.*"
  • ""
  • "*"
IoC Reference Reference to a model path in another component
  • "{otherComponent}.model.modelPath"
  • "{otherComponent}.model.modelPath.*"
  • "{otherComponent}.model"
  • "{otherComponent}.model.*"

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
  • segs (Array - required) and
  • context (String - optional) which encodes the model path to be matched.
These examples encode exactly the same path expressions, in the same order as in the section Model Path References
  • {segs: ["modelPath"]}
  • {segs: ["modelPath", "*"]}
  • {segs: []}
  • {segs: ["*"]}
  • {segs: ["modelPath"], context: "otherComponent"}
  • {segs: ["modelPath", "*"], context: "otherComponent"}
  • {segs: [], context: "otherComponent"}
  • {segs: ["*"], context: "otherComponent"}
Array An array of <modelPathReference>: members are either a String holding a <shortModelPathReference> or an Object <modelPathRecord>. The listener will be notified when any of these paths receive changes. Below is further information on 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 any of these paths receive changes.

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:
  • "modelPath"
  • ""
  • "{otherComponent}.model.modelPath"
  • "{otherComponent}.model"
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 sources 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.