-
Notifications
You must be signed in to change notification settings - Fork 3
EventBus, Dispatcher & Events
Major part of development in JavaScript relies on events. It's easy solution to notify distant instances or React elements scattered across the DOM tree.
To ease the pain that comes with events, IMA.js provides a two utilities that take care of firing and receiving events. Each one with a different style.
The ima/event/EventBus
allows your UI components to emit custom DOM events
that naturally propagate through the DOM tree representing the tree of your UI
components.
This is used to notify the parent components of user interaction with custom controls in your UI, or to notify the page Controller itself.
The custom events may have any name and carry arbitrary data that are not restricted to JSON-serializable values.
EventBus
can be used in View and Components via fire()
method that is
inherited from ima/page/AbstractComponent
.
// app/component/expandable/ExpandLink.jsx
onClick(event) {
const { expandableId } = this.props;
this.fire('expand', { expandableId }); // this will be stopped by ExpandableWrapper
}
render() {
return (
<button onClick = { event => this.onClick(event) }>
{ this.props.children }
</button>
);
}
The opposite for the fire()
method is listen()
and unlisten()
. First
argument for the listen()
and unlisten()
method is an element the listener
will be bound to. It can be either DOM element or a React Component that is
mounted in the DOM tree.
// app/component/expandable/ExpandableWrapper.jsx
constructor() {
this._expandListener = this._expandWrapper.bind(this);
}
componentDidMount() {
this.listen(this, 'expand', this._expandListener);
}
componentWillUnmount() {
this.unlisten(this, 'expand', this._expandListener);
}
_expandWrapper(event) {
if (event.data.expandableId !== this.expandableId) {
return; // this expand event is not meant for us.
}
// Do to propagate the event further.
event.stopPropagation();
// Do the expand!
}
render() {
return (
<div className = 'expandWrapper'>
// ... ExpandLink can be nested any level deep. Otherwise we could use simple props callback
<ExpandLink expandableId = { this.expandableId }>
See more
</ExpandLink>
</div>
);
}
Furthermore, the Controllers can easily listen for the events dispatched using
the EventBus
(unless the propagation of the event is stopped by a component
half the way) by declaring event listener methods.
An event listener method is a method of a controller named by the first-letter
capitalized event name with the on
prefix, for example the formSubmitted
event can be listened for by defining the onFormSubmitted()
method on your
controller.
The first argument passed into the controller's event listener method will be the event data, not the event object itself, as manipulating the event object once it reaches the controller is pointless.
// app/page/article/ArticleController.js
onExpand({ expandableId }) {
// Event never reaches this point because we issued
// event.stopPropagation() in ExpandableWrapper.jsx
}
The obvious limitation of the ima/event/EventBus
API is that it only allows
to create events that propagate up the tree of the UI components. The common
way to propagate event in other directions, or to other parts of the UI, or
from the controller to the UI is using the app/event/Dispatcher
API.
Accessing Dispatcher in Controllers is easy with Dependency Injection.
To access Dispatcher from Views and Components you should register it in ComponentUtils.
// app/config/bind.js
import Dispatcher from 'ima/event/Dispatcher';
export let init = (ns, oc, config) => {
const ComponentUtils = oc.get('$ComponentUtils');
ComponentUtils.register({
$Dispatcher: Dispatcher
});
}
The Dispatcher allows any UI component and controller to register and deregister
event listeners for arbitrarily named events using listen()
and unlisten()
methods and fire these events with arbitrary data using fire()
method.
Events propagate directly to the registered event listeners with no way to stop their propagation.
// app/component/image/Image.jsx
onImageClick(event) {
event.preventDefault();
const { image } = this.props;
this.utils.$Dispatcher.fire('showLightbox', { image });
}
// app/component/lightbox/Lightbox.jsx
componentDidMount() {
this.utils.$Dispatcher.listen('showLightbox', this.onLightboxShow, this);
}
componentWillUnmount() {
this.utils.$Dispatcher.unlisten('showLightbox', this.onLightboxShow, this);
}
onLightboxShow(data) {
// ...
}
Note: Lightbox component can be mounted anywhere in the DOM tree and it will still receive the
showLightbox
event when it's fired.
Note: A great place to mount components like Lightbox is ManagedRootView.
Note that events distributed using the Dispatcher are useful only in very specific use-cases, so the Dispatcher logs a warning to the console if there are no listeners registered for the fired event in order to notify you of possible typos in event names.
IMA.js fires a few events that let you know that something has happened under the hood. You can listen to these events using Dispatcher's listen method.
This event is fired after the router matches new url to a registered route and before the page change starts. The data passed with the event look like this:
{
// the new Route instance
route,
// new URL parameters
params,
// the path portion of the new URL (a route definition is matched against this)
path,
// route options extended of options provided to the function
// that triggered the routing
options,
// an action object describing what triggered the routing
// if a PopStateEvent triggered the routing the action object will
// look like this { type: 'popstate', event: PopStateEvent }
// otherwise the event will be MouseEvent (e.g. clicked on a link)
// and type will be either 'redirect', 'click' or 'error'.
action
}
This event is contrary to the BEFORE_HANDLE_ROUTE
. It's fired with the same
data but after the page was changed.
Note: The
AFTER_HANDLE_ROUTE
event will be fired regardless if promises loaded be the new Controller are resolved.
An event fired before the page state changes. The handler of this event receives following data:
{
// The state object derived from the oldState and patchState
newState,
// The current state
oldState,
// The data that were passed to the `setState` method
patchState
}
Note: You can mutate the
newState
object if you wish. MutatingoldState
andpatchState
will have no effect.
An event fired after the page state changes. The data passed with this event
contain only the newState
object.
PageRenderer fires this event after current page view is mounted to the DOM. Event's data contain { type: String }
Where type can be one of constants located in @ima/core/page/renderer/Types
.
PageRenderer fires this event after current state is updated in the DOM. Event's data contain { state: Object<string, *>}
.
PageRenderer fires this event after current view is unmounted from the DOM. Event's data contain { type: String }
Where type can be one of constants located in @ima/core/page/renderer/Types
.
PageRenderer fires this event when there is no _viewContainer in _renderToDOM method. Event's data contain { message: string }
.
- Introduction
- Static view
- Adding some state
- Fetching the data from the server
- Writing posts
- Final polish