Event injection and boiling
This functionality is Sneak Peek status. This means that the APIs may change. We welcome your feedback, ideas, and code, but please use caution if you use this new functionality.
New in v1.4
The Infusion Event System explains how to declare events of various types attached to a single component. Within a larger design, sometimes it is necessary to
- collaborate between multiple components in a component tree on "sharing" references to event firers ("event injection")
- present an event with a particular signature fired by a component as one with a different signature in a listener ("event boiling")
Both of these capabilities rely on the Inversion of Control (IoC) system and on the tree of components in question being an IoC-driven tree.
Event injection
Event injection comes in two different forms. The first form is the wholesale injection of an event belonging to one component, to appear as an event belonging to another. These events will share a single set of listeners which will be fired when any sharing component fires the event, and any sharing component may add and remove listeners. This is achieved by simply referencing the source event to be injected on the right hand side of the standard events
block on the component. For example:
fluid.defaults("fluid.tests.parentComponent", { gradeNames: ["fluid.eventedComponent", "autoInit"], events: { parentEvent: null }, components: { childComponent: { type: "fluid.tests.childComponent" } } }); fluid.defaults("fluid.tests.childComponent", { gradeNames: ["fluid.eventedComponent", "autoInit"], events: { parentEvent: "{parentComponent}.events.parentEvent" } });
The first defaults block in this example defines a standard eventedComponent which defines one concrete event, named parentEvent
, and one child component, with options unspecified.
The second defaults block defines defaults for childComponent
. Here, the reference to the event parentEvent
becomes shared between the two components. childComponent
will appear to have a event firer named parentEvent
which behaves exactly as if it were defined locally (as it is in the parent component) and will share listeners with the parent component.
Proper scoping for event binding
Note that the defaults we have written here for childComponent
reflect a bad practice - a "blind reference" to a parent component which may or may not actually be in scope when the child is instantiated. This form of definition was just written for clarity here, but in fact prevents the childComponent
component from being used in contexts where it could not find parentComponent
. A sounder way of expressing this relationship is to write the injection of parentEvent
in a demands block which is scoped so that it only binds in a context where parentComponent
component is actually present. This would be done with the following pair of definitions:
fluid.defaults("fluid.tests.childComponent", { gradeNames: ["fluid.eventedComponent", "autoInit"], events: { parentEvent: null } }); fluid.demands("fluid.tests.childComponent", "fluid.tests.parentComponent", { options: { events: { parentEvent: "{parentComponent}.events.parentEvent" } } });
Here, the base component childComponent
contains a standard local definition of an event named parentEvent
which satisfies the component's own requirements to fire this event when in isolation. The demands block acts when a component of type "fluid.tests.parentComponent"
can be found, and uses the mergePaths directive to express that this demands block contains options material which should be merged on top of the component's defaults. This overwrites the definition of the local event parentEvent
with a directive to inject the parent's event parentEvent
instead.
Event boiling
A "Boiled" event is derived from another event (a "base event") but allows the signature of the event to be adjusted. A listener to a boiled event receives a call at the same point in time, but can receive a different set of arguments than the ones which were supplied in the original call to fire()
which triggered the event. This modified argument set can draw values from IoC-resolved contextual values around the component tree, as well as from the original argument set which the firer of the event supplied.
Boiled events are useful in wiring together consumers and producers of events who have different expectations - these differences can arise, for example, through the development of the codebases being in different lifecycles - perhaps the producer of the event is part of framework code which is not going to be updated for a long time, but has been written with a poorly planned API which does not expose crucial information which the event consumer requires.
Suggestions are still welcomed for more a suitable name than "boiled events".
Boiling in defaults
A boiled event is defined in just the same place as a standard event - in the "events" block of a component's defaults. The configuration ("right-hand side") value is more complex than that for a simple event - it needs to specify not only the base event, but also the transformation performed on the argument list. The configured value must contain two elements, the event
property, which references the event to be boiled, and the args
which specifies the argument list which will be received by listeners to the boiled event. This uses the standard {context}.pathName
format for contextualised EL values which is used in IoC, with the addition that one extra context object is in scope - the context {arguments
} allows the argument list to refer to the original argument list that was presented when the base event was fired. For example:
fluid.defaults("fluid.tests.eventBoiled", { gradeNames: ["fluid.eventedComponent", "autoInit"], events: { boiledLocal: { event: "localEvent", args: ["{arguments}.1", "{arguments}.0"] }, localEvent: null } });
In this code block, the component defines two events - one "basic event" named localEvent
, and one "boiled event" named boiledLocal
which uses localEvent
as a base. In this case, a listener registered to boiledLocal
will receive the first two arguments which were supplied when localEvent
was fired, but swapped to appear in the opposite order.
Boiling in demands
The boiling specification for an event may also appear in a demands block rather than in a component's defaults. This caters for the case that different signatures are required for events fired from the same component where it appears in different contexts.
fluid.defaults("fluid.tests.eventBoiled", { gradeNames: ["fluid.eventedComponent", "autoInit"], events: { boiledLocal: { event: "localEvent" }, localEvent: null }); fluid.demands("boiledLocal", "fluid.tests.eventBoiled", ["{arguments}.0", "{arguments}.1"]);
It's necessary to supply some specification in the parent that the event will be boiled - in particular, the base event that it derives from. However, the actual argument specification can be deferred to the demands block, using the standard syntax for arguments in demands blocks.