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 or listener boiling)
Both of these capabilities rely on the IoC - Inversion of Control system.
Listener Boiling
Receiving a Different Signature in the Listener
It often occurs that the signature of a function registered as a listener doesn't match the one fired by the event. Adapting to this mismatch in configuration is sometimes referred to as listener boiling. The syntax used for listener boiling is identical to that applied for defining Invokers.
Here is an example of a component which defines a single event, simpleEvent
- the firer for it uses a signature (int value, boolean flag)
but we have a listener that requires a different signature (Object that, int value)
where the first
argument consists of the component itself and the 2nd argument consists of the supplied first argument:
examples.externalListener = function (that, value) {
console.log("Received value ", value, " fired from component ", that);
};
fluid.defaults("examples.boiledListenerComponent", {
gradeNames: "fluid.component",
events: {
simpleEvent: null
},
listeners: {
simpleEvent: {
funcName: "examples.externalListener",
args: ["{that}", "{arguments}.0"]
}
}
});
var that = examples.boiledListenerComponent();
that.events.simpleEvent.fire(5, true); // listener above will log 5, that
Injecting a Listener to an Event Elsewhere in the Tree
Either together with or separate from the adjustment of listener signatures in the previous section, it is also possible to attach a listener to a component elsewhere in the tree than the one where the listener configuration is written. Note that with this built-in syntax the listener can only be injected into (registered as a listener to) a component which is visible as a parent or a sibling of the current component, using a standard upward-matching IoC Context Selector. If you need the more powerful facility to inject a listener downwards (that is, to one or more components that may not yet be constructed) please see the section describing the use of the distributeOptions options block.
fluid.defaults("examples.injectedListenerParent", {
gradeNames: "fluid.component",
events: {
parentEvent: null
},
components: {
child: {
type: "fluid.component",
options: {
listeners: {
"{injectedListenerParent}.events.parentEvent": "examples.externalListener"
}
}
}
}
});
var that = examples.injectedListenerParent();
that.events.parentEvent.fire(that, 5); // strikes above listener through injected listener attachment
Note: Both of these kinds of boiling can be applied at the same time - that is, it is possible to
adjust the signature of a listener using args
at the same time as resolving to it elsewhere in the tree by means of an
IoC reference key. Note also that all injected listeners automatically deregistered by the framework when the component
which holds their record (e.g. the child
component in this example) is destroyed - there is no need for the user to
call removeListener
manually.
Event Injection
Event injection 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 referencing the source event to be
injected on the right hand side of the standard events
block on the component. For example:
Example 1
The first defaults block in this example defines a plain component 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.
fluid.defaults("fluid.tests.parentComponent", {
gradeNames: "fluid.component",
events: {
parentEvent: null
},
components: {
childComponent: {
type: "fluid.tests.childComponent"
}
}
});
fluid.defaults("fluid.tests.childComponent", {
gradeNames: "fluid.component",
events: {
parentEvent: "{parentComponent}.events.parentEvent"
}
});
Note: This technique is not generally recommended because of the possibility for unexpected effects when registering listeners with namespaces. Since an injected event is exactly the same event as the original, namespaced listeners registered across the different sites will be deduplicated, meaning that only one will be registered per namespace. This is often an unexpected behaviour, so instead a more lightweight technique of remotely registering listeners as described in the previous section is preferred. Alternatively, you may register the firer of one event as a listener to another, or used boiled events as described in the next section.
Alternative Choice of Scoping For Event Binding
Note that the defaults we have written in the examples above for childComponent
prevent it from being used in contexts
where the reference {parentComponent}
cannot be resolved. This may or may not be desirable depending on the purpose
for childComponent
. Its purpose may only comprise being reused together with {parentComponent}
, or it may be
intended to be more generally reusable - since this is only test/example code, this intention can't be made clear. For
completeness, we present another style of writing this configuration which expresses the other intention, that
childComponent
should be generally reusable: this moves the reference to parentComponent
into the tree for
parentComponent
itself, ensuring that childComponent
can be constructed without the parent if required.
Example 2
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 event binding on parentEvent
is
defined in the defaults block of parentComponent
. At the instantiation of parentComponent
, the bound event
overwrites the definition of the local event parentEvent
.
fluid.defaults("fluid.tests.parentComponent", {
gradeNames: "fluid.component",
events: {
parentEvent: null
},
components: {
childComponent: {
type: "fluid.tests.childComponent",
options: {
events:{
parentEvent: "{parentComponent}.events.parentEvent"
}
}
}
}
});
fluid.defaults("fluid.tests.childComponent", {
gradeNames: "fluid.component",
events: {
parentEvent: null
}
});
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 as a standard listener, 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 a more suitable name than boiled events. Because boiled events are distinct from their sources, they do not suffer from the namespacing risks highlighted for injected events.
Boiling One Single Event
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:
Example 3
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.
fluid.defaults("fluid.tests.eventBoiled", {
gradeNames: "fluid.component",
events: {
boiledLocal: {
event: "localEvent",
args: ["{arguments}.1", "{arguments}.0"]
},
localEvent: null
}
});
Boiling Multiple Events
The event boiling can be used to boil multiple events that are either from the same or a different component.
Example 4
The example below shows boiling from a different component, the boiled event boiledDouble
in the example will be fired
once both parentEvent1
and parentEvent2
from parentComponent
are fired. The arguments supplied to boiledDouble
would be the first argument provided by parentEvent1
and the second argument provided by parentEvent2
.
fluid.defaults("fluid.tests.parentComponent", {
gradeNames: "fluid.component",
events: {
parentEvent1: null,
parentEvent2: null
},
components: {
child: {
type: "fluid.tests.childComponent",
options: {
events: {
boiledDouble: {
events: {
event1: "{parentComponent}.events.parentEvent1",
event2: "{parentComponent}.events.parentEvent2"
},
args: ["{arguments}.event1.0", "{arguments}.event2.1"]
}
}
}
}
}
});
fluid.defaults("fluid.tests.eventChild3", {
gradeNames: "fluid.component",
events: {
boiledDouble: null
}
});
The same syntax can be used to boil events from the same component, where the references to source events can be simplified by specifying the events directly without IoC references, as shown in Example 3, since both source events and boiled event are residing on the same component.