IoCSS

The Infusion Inversion of Control (IoC) system includes a simple method for distributing options from any components to any others in the component tree. This system uses a special block named distributeOptions which is supported in the options of every Infusion component. The key addressing scheme needed to make this system work — the means for determining where the target of the distribution is — is dubbed "IoCSS" because it uses a syntax and idiom very similar to that used in CSS selectors.

distributeOptions and IoCSS: Downward-matching CSS-like context selectors for options forwarding

The distributeOptions option is a top-level block supported by every Infusion component. It specifies how options should be distributed to components elsewhere in the tree. A typical, conservative usage would have a component distribute only downwards to its own subcomponents, but it is also possible to use the considerable power of distributeOptions to distribute to any component anywhere in the (global) component tree - naturally such power should be used with extreme discretion since it could very easily reduce a design to chaos. However, in many situations, for example when writing powerful authoring tools, this kind of power is completely indispensible.

Background for distributeOptions

As component trees become larger, it will often happen that a high-level component will need to specify options for a component further down the component tree. Without distributeOptions, component configuration can become very, what we might call, "pointy":

// without developer's use of IoCSS, user writes

fluid.uiOptions(".my-uio-container", {
    components: {
        templateLoader: {
            options: {
                templatePrefix: "../../myTemplates"
            }
        }
    }
});

The example above shows a simplified version of a situation with Infusion's "UI Options" component. In practice, the user of the component would have to write an even more deeply nested piece of configuration than this, if the developer had not made use of the distributeOptions directive in the component's options block, together with the use of "IoCSS" expressions to distribute the user's options to the right place in the component tree, as shown in the following example:

// developer writes:

fluid.defaults("fluid.uiOptions", {
    distributeOptions: {
        source: "{that}.options.templatePrefix",
        target: "{that templateLoader}.options.templatePrefix"
    }
});

// user writes:

fluid.uiOptions(".my-uio-container", {
    templatePrefix: "../../myTemplates"
});

In the distributeOptions block above, the context {that templateLoader} is an IoCSS expression which designates one or more of the child components of UI Options that are to receive the user's option. The syntax and meaning of these expressions is defined below.

As well as converting the exposed options structure of a component into a more compact form, distributeOptions is also a powerful tool for maintaining API stability for a component or family of components. Since the binding of IoCSS selectors such as that templateLoader onto child components is flexible, the component tree could be refactored in quite an aggressive way without requiring changes in either the user's configuration, or even the distributeOptions block itself. If the refactoring was even more thorough (involving wholesale removal of the target component, or a change in its important grades), the developer could still maintain stability of the external user API just by changing the distributeOptions block.

In terms of a standard discussion on Design Patterns, this use of distributeOptions could be seen as an automated and declarative scheme for achieving the ends of the Facade Design Pattern, without the need for either user or developer code.

However, facade formation isn't the only possible function of an options distribution. They are also sufficiently powerful to encompass most uses of the so-called Aspect-oriented programming (AOP) by the use of the target root context / to match (and "advise", in AOP terminology) all components in a design matching a particular specification. Unlike traditional AOP techniques, however, these options distributions do not put ultimate and final power in the hands of the wielder - since the component holding the distribution itself can be either advised itself, or destroyed completely - thus withdrawing the "advice" from the system.

distributeOptions format

The distributeOptions option is a top-level block supported by every Infusion component, holding an array of records, hash of records, or single record containing the following properties:

Name Description
target (Required) An IoC expression describing the location in the component tree where the options are to be set. The "context" part of this expression will usually consist of an IoCSS selector (see below for format). However, it may also in specialised cases consist of a standard "upwards" IoC context expression, indicating that options are to be distributed to a parent component (this is only meaningful in the case the parent has not finished instantiating).
record (Mutually exclusive with source) A record of options to set at the target. This will be interpreted literally - however it will be expanded according to standard options expansion rules. General configuration will be expanded in the context of the component holding distributeOptions - however, configuration for listeners and invokers will be expanded at the target component.
source (Mutually exclusive with record) An IoC expression into the options structure of the source component, referencing what to copy to the target.
  removeSource (Only possible if source is used) true/false: If true, the source options block is removed from its original site in the options structure when it is forwarded to the target.
  exclusions (Only possible if source is used) A list of EL paths into the source material which should not be forwarded. Whether or not removeSource is used, these will be retained in their original position in the source component's options.
priority (Optional) a Priority specification specifying how the priority of this distribution is to be resolved with respect to any other options distributions which are targetted at the same component in the tree as this one. Typically one will write after:otherNamespace to indicate that this distribution should be stronger than (i.e. should merge options on top of) another distribution whose namespace is otherNamespace, or before:otherNamespace to indicate that this distribution should be weaker than (have option merged on top of by) a distribution whose namespace is otherNamespace.
namespace (Optional, recommended) A namespace to identify this options distribution amongst others in an extended design. This can be used by the priority field of other distributions in order to defer to it or be deferred to by it. (The framework should also ensure to uniquify distributions with respect to namespace at the target - not currently implemented)

In the case that a hash of these records is provided, the keys of the structure will be interpreted as the namespace of the distribution.

IoCSS Selectors

An IoCSS selector appears in some special kinds of references in the framework, most notably in the target element of a distributeOptions block. Informally, it can be distinguished from a standard IoC reference since it contains some whitespace between its various matching segments, and a standard IoC reference just consists of a single context name without whitespace. Some representative examples of IoCSS selectors are:

  • {that sessionManager} - matches any component which matches the context sessionManager below the current component
  • {that > ownSub} - matches any component directly a child of the current one matching a context ownSub (perhaps by being a member with that name)
  • {testEnvironment flowManager preferencesServer} - firstly looks upward to find a testEnvironment component somewhere above the current one, and then matches a preferencesServer that is nested within a flowManager
  • {/ fluid.viewComponent} - matches any fluid.viewComponent anywhere in the component tree

Each block separated by whitespace matches a component by one of the following component selector rules:

Form Description
* matches any component - universal selector
/ matches the global root component of the entire component tree - may only be used in the head position of an IoCSS selector
E or &E matches any component holding a context name of E - the & character may be omitted for the first context in a group. There is special support for the context that as with standard IoC context matching
#myid matches the component with id equal to myid (of no use to developers since component ids cannot be predicted)

Mediating each neighbouring pair of component rules is a descendent rule, of which we currently support 2:

Form Description
E F Matches any F component that is a descendant of an E component
E > F Matches any F component that is a direct child of an E component

If you are familiar with CSS selectors you can compare this set of 6 simple syntax rules with the wide range of CSS selector rules listed in the article's table.

Simple Example of distributeOptions: record for grade distribution

This is the form taken by the most commonly seen and straightforward use of distributeOptions - the contribution of an additional grade to one or more subcomponents of the current component:

fluid.defaults("kettle.use.session.io", {
    gradeNames: ["fluid.component"],
    distributeOptions: {
        record: "kettle.sessionManager.io",
        target: "{that sessionManager}.options.gradeNames"
    }
});

The sessionManager, wherever it is in the set of child components, will be granted the additional grade kettle.sessionManager.io.

Complex Example: record

fluid.defaults("fluid.moduleLayoutHandler", {
    gradeNames: ["fluid.layoutHandler"],
    // ...
    distributeOptions: {
    // unusual: not an IoCSS selector - upward-matching selector distributes options back to parent before instantiation ends
        target: "{reorderer}.options",
        record: {
            selectors: {
                movables: {
                    expander: {
                        func: "{that}.makeComputeModules",
                        args: [false]
                    }
                },
                dropTargets: {
                    expander: {
                        func: "{that}.makeComputeModules",
                        args: [false]
                    }
                },
                selectables: {
                    expander: {
                        func: "{that}.makeComputeModules",
                        args: [true]
                    }
                }
            }
        }
    }
});

Example: source

fluid.defaults("fluid.tests.uploader", {
    gradeNames: ["fluid.component"],
    components: {
        uploaderContext: {
            type: "fluid.progressiveCheckerForComponent",
            options: {componentName: "fluid.tests.uploader"}
        },
        uploaderImpl: {
            type: "fluid.tests.uploaderImpl"
        }
    },
    distributeOptions: [{
        target: "{that > uploaderImpl}.options", // Target a directly nested component matching the context "uploaderImpl"
        source: "{that}.options", // Distribute ALL of our options there, except exclusions:
        exclusions: ["components.uploaderContext", "components.uploaderImpl"] // options targetted directly at these subcomponents are left undisturbed in place

    }],
    progressiveCheckerOptions: {
        checks: [{
            feature: "{fluid.test}",
            contextName: "fluid.uploader.html5"
        }]
    }
});

Example: broadcast via root to all components

(All Husnock, everywhere)


// A `mixin grade` to contribute two listeners to every viewComponent in the system
fluid.defaults("fluid.debug.listeningView", {
    listeners: {
        onCreate: {
            funcName: "fluid.debug.viewMapper.registerView",
            args: ["{fluid.debug.viewMapper}", "{that}", "add"]
        },
        onDestroy: {
            funcName: "fluid.debug.viewMapper.registerView",
            args: ["{fluid.debug.viewMapper}", "{that}", "remove"]
        }
    }
});

fluid.defaults("fluid.debug.viewMapper", {
    gradeNames: ["fluid.component", "fluid.resolveRoot"],
    // Distribute the grade `fluid.debug.listeningView` to every viewComponent in the system
    distributeOptions: {
        record: "fluid.debug.listeningView",
        target: "{/ fluid.viewComponent}.options.gradeNames" // <- use of IoCSS root context "/"
    }
});