Skip to content

Instantly share code, notes, and snippets.

@amb26
Created August 11, 2014 14:59
Show Gist options
  • Save amb26/64876ac4cea10c690dce to your computer and use it in GitHub Desktop.
Save amb26/64876ac4cea10c690dce to your computer and use it in GitHub Desktop.

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 - many more declarative uses are supported in Infusion 1.5 and even more will be supported in Infusion 2.0. This page presents both programmatic calls and their declarative equivalents where they exist.

Registering interest in model changes using a ChangeApplier

Declarative style

The declarative style for registering interest in change events uses an entry in the modelListeners options area of a modelRelayComponent. 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 <modelPathReference>: <modelListener declaration>. The left and right hand sides of this definition will be explained in the subsequent sections:

Model Path References

A <modelPathReference> has the form:

Syntax definition for <modelPathReference> - the key in modelListeners options block for a modelRelayComponent
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 4 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.

Model Listener Declaration

A model listener declaration block has exactly 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. There are two extra features that are supported in model listener blocks that are not supported in standard listener declarations. These are the special context name change, and the ability to filter a change based on its source.

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 next section)

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. The three currently supported sources are init, relay and local - custom user-defined sources may be supported in the future.

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 sources supported as values in excludeSource and includeSource are as follows:

Values for 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

Example featuring source filtering:

fluid.defaults("examples.sourceExample1", {
    gradeNames: ["fluid.modelRelayComponent", "autoInit"],
    model: "initial value",
    modelListeners: {
        things: {
            funcName: "console.log",
            excludeSource: "init",
            args: "{change}.value"
        }
    }
});

var that = examples.sourceExample1();
that.applier.change("things", "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.

Wildcards in model path references

The last path segment of a model path reference may or may not be "*". Whether it is "*" 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.modelRelayComponent", "autoInit"],
    modelListeners: {
        things: {
            funcName: "console.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.modelRelayComponent", "autoInit"],
    modelListeners: {
        "things.*": {
            funcName: "console.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

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, pathSpec - this holds the same model path reference documented in the previous section on declarative binding:

applier.modelChanged.addListener(pathSpec, listener, namespace)
applier.modelChanged.removeListener(listener)

This style of listening to changes is not recommended. Since such a listener can only be attached once a component and its applier have been fully constructed, it will miss observation of the initial transaction as well as any other model changes that have occurred up to the point where it is registered. This implies that the state of the model that it sees cannot be fully predicted without a knowledge of the entire surrounding of the component tree.

The listener is notified after the change (or set of coordinated changes) has already been applied to the model - it is too late to affect this process and so this event is not preventable. The signature for these listeners is

function listener(value, oldValue, pathSegs, changeRequests)
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
changeRequests A single ChangeRequest object or an array of them (see below) which were responsible for this change (may be empty)

Users will in many cases only be interested in the first argument in this signature.

Firing a change using a ChangeApplier

Declarative style

The declarative style for firing model changes involve a kind of IoC 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 which determines which path in which component model will receive the change.

Change record for firing changes by declarative binding
Member Type Description
changePath <model path reference> (String) 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.

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.modelRelayComponent", "autoInit"],
    model: "initialValue",
    invokers: {
        changer: {
            changePath: "",
            value: "{arguments}.0"
        }
    }
});
 
var that = examples.changeExample();
that.changer("finalValue");
console.log(that.model); // "finalValue"

Programmatic style

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)
Path Type Description
path String An EL path into the model where the change is to occur.
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)

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"} 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 3 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 in an event pipeline within the framework.

The programmatic style for firing changes is not strongly discouraged, as 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, and the framework will support more and more schemes for declarative triggering of changes moving up to Infusion 2.0.

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 new declarative model listener registration syntax in Infusion 1.5:

fluid.defaults("my.component", {
    gradeNames: ["fluid.modelComponent", "fluid.eventedComponent", "autoInit"],
     
    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 possible but is not recommended for general use. In particular, manual instantiation of a ChangeApplier will not allow access to the powerful Model Relay framework features, which require the ChangeApplier to be properly situated in an IoC component tree. However, it's possible to imagine some cases where authors of "superframeworks" might find this facility useful. In Infusion 1.5 there are several variants for construction of ChangeAppliers since the implementation is in transition from the old non-relay-aware applier to the new model relay implementation.

var applier = fluid.makeChangeApplier(model, options); // currently produces an "old-style" applier
 
    OR
var applier = fluid.makeHolderChangeApplier(holder, options); // currently produces an "old-style" applier
    OR
var applier = fluid.makeNewChangeApplier(holder, options); // produces a new "relay-aware" applier

In every case the options argument is optional. Where the parameter model appears it directly represents the model to be managed by the applier. Where parameter holder appears, it represents instead an object which is expected to be a container for the model where the model itself is held in this object at the member named model. In framework use this argument will actually hold the owning component itself, although the applier makes no use of any properties of this object other than the one held at model.

Under the old ChangeApplier semantics, the model handle was closed over and remained constant for the lifetime of the applier. The framework contained utilities such a fluid.model.copyModel which assisted the user in working with a model with changing contents whilst keeping its base reference constant. Under the new ChangeApplier semantics, the model base reference is not stable and in fact starts by holding the value undefined as every model component initialises.

Operating transactions manually

Similarly to manual construction of a ChangeApplier, this is not recommended for normal users of the framework. It may be appropriate for advanced users of the framework who are building higher-level frameworks layered over it.

A transaction can be opened using the initiate() method of the applier function which returns a transaction object:

var myApplier = fluid.makeChangeApplier(myModel);
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment