Events and Publish-Subscribe
The key concept that distinguishes LaxarJS applications from other web applications is the publish-subscribe (or pub/sub) architecture. This approach allows to isolate building blocks such as widgets and activities by moving the coupling from implementation (no module imports, no service contracts) to configuration (of event topics).
Preliminary readings:
LaxarJS consistently uses the term events rather than messages, to point out two key aspects of its pub/sub-approach:
-
events convey information about what happened (rather than who is receiver),
-
delivery is always asynchronous (using an event loop).
For these reasons, you may also think of this pattern as a variation on the Hollywood principle ("Don't call us, we'll call you").
For efficient processing, LaxarJS technology adapters tie into the change detection of their respective frameworks.
For example, the "angular"
adapter triggers an AngularJS $digest
-cycle after events were delivered.
This allows the web browser to batch event-handling with other operations that modify screen contents.
The Event Bus
All events are published to and delivered by the event bus: The event bus manages name-based (aka topic-based) event subscriptions for all interested widgets and activities (the subscribers): Subscribers specify an event name pattern that tells the event bus which kinds of "thing that happened" they are interested in. When an event is published to the event bus, it is kept in an event queue, to be delivered asynchronously. During event delivery, each event name is matched against each subscription, and each matching event is delivered by running the associated callback.
Each event has a name containing a summary of what happened, and a payload carrying additional information.
Event Names
Event names summarize what happened, possibly with some additional context. They follow a hierarchical structure that is used to pattern-match against subscriptions during delivery.
An event name is a string, formed by a sequence of one or more topics that are separated by .
(the full stop, U+002E).
Each topic is a string, made up from a sequence of one or more sub-topics separated by -
(the hyphen-minus, U+00AF).
Sub-Topics are strings, formed by
- either an upper case letter followed by a sequence of upper case letters and numbers
- or a lower case letter followed by a sequence of mixed case letters and numbers
These rules also exist as a formal grammar for thorough people.
These are examples for valid event names:
didReplace.myShoppingCart
takeActionRequest.searchArticles
didTakeAction.searchArticles.SUCCESS
willEndLifecycle
didValidate.popup-user2
Invalid event names include:
DidReplace.myShoppingCart
: invalid, first topic starts upper case but contains lower case letters.searchArticles.SUCCESS
: invalid, empty topic is not alloweddidUpdate.1up
: invalid, topic must not start with a number
Naming Best Practices and Event Patterns
Good event names start with a very general verb-based first topic, broadly describing what happened.
That topic is often followed by a more specific object-based second topic, describing where (or to what) something happened.
Sometimes, this second topic is broken down into sub-topics that allow to "zoom in" on the event details.
For example, the event didValidate.popup-user2 informs all interested subscribers, that the second user has been validated by a widget within a popup.
This information can now be used to show validation messages at the appropriate location.
Sometimes there is a modal third topic, broadly describing how something happened (e.g. to communicate an outcome such as SUCCESS
or ERROR
).
Of course, nothing prevents senders to break these rules and use any structure for their event names as long as they conform to the grammar. But for best interoperability between widgets and activities, not only should the general structure of event names be observed.
It is recommended wherever possible for widgets to use one or more of the established event patterns: These patterns consist of event vocabularies and minimal associated semantics that have been identified during the development of LaxarJS. A few core patterns are baked right into the LaxarJS runtime, and these are listed below. Other useful patterns are described in the separate project LaxarJS Patterns. Even if not using the LaxarJS Patterns library, widget authors are very much encouraged to use its event vocabularies whenever meaningful.
Event Payload
An event does not only have a name, but also a payload.
Any JavaScript object that can be directly represented as JSON can be used as a payload.
This allows for the object to contain instances of string, array, number, boolean and object, including null
.
On the other hand, it excludesundefined
, Date, RegExp and custom classes.
The event bus will create a copy of the payload for each subscriber that gets the event delivered. This improves decoupling and robustness, because events are "fire and forget": A widget may publish some resource through an event and afterwards immediately modify its contents, but all subscribers are guaranteed to receive the original event.
However, this also means that you should only publish resources that are at most ~100 kilobyte in size. For larger resources, it is recommended to only transfer a URL so that interested widgets may receive the content from a server (or the browser cache), if required.
Two-Way Communication or the Request/Will/Did Mechanism
Sometimes a widget wants some other widget or activity on the page to perform some action. This might be a longer running action such as a search or some server side validation. The requesting widget does not care about who actually performs the request, but it is interested in when the request has been fully processed by all respondents, and what is the outcome.
As an example, consider a multi-part user sign-up process, where each of several widgets allows the user to enter and validate some of the information such as email address, payment information or a CAPTCHA. Another widget offering a "Complete Sign-Up" button would be responsible for the overall process of submitting the registration resource to a REST service and navigating to a different page. Before hitting the registration service, this widget would ask all input widgets to validate their respective sign-up details in order to provide immediate feedback to the user. Some of the widgets might have to query their own validation services though, such as the aforementioned CAPTCHA-using widget.
Using the Request/Will/Did mechanism, such functionality can be achieved without the registration widget having to know any of the participant widgets:
-
The individual widgets are configured on the page to work with a common
registrationForm
resource. On instantiation, the input widgets offering validation subscribe tovalidateRequest
events for this resource. -
When the user activates the Complete Sign-Up button, the registration widget issues a
validateRequest.registrationForm
event, indicating that- a validation has been requested (what happened) and
- it concerns the resource
registrationForm
(where it happened).
The registration widget may now disable its button and start showing an activity indicator to help the user recognize that an action is in progress.
-
During delivery, the input widgets supporting validation receive the request and publish a
willValidate.registrationForm
event to indicate that- a validation has been initiated (what) and
- that it concerns the
registrationForm
resource (where).
-
Each widget will either call its registration service to respond asynchronously, or publish a response directly if it can validate locally. The response is either
didValidate.registrationForm.SUCCESS
ordidValidate.registrationForm.ERROR
conveying that- a validation has been performed (what) and
- that it concerns the
registrationForm
resource (where) and - the way the validation turned out (how).
-
Once all responses have been collected and there were no validation errors, the registration form will be notified (through a promise) and the sign-up REST request may be performed.
This mechanism allows any of the widgets on the page may be removed or replaced without any of the other widgets having to know.
New widgets may be added at any time, and will work as long as they support the validation pattern.
For example, the message display widget could be added to gather and display validation messages to the user, simply by hooking it up to the same resource and processing its "didValidate"
events.
Even if some widgets do not support the validation pattern, they can still be used, only that their validation would have to be handled by the server upon submission of the registration form.
Validation and other patterns are described in the following section.
Pattern Reference
A few event patterns are supported directly by LaxarJS, while others are described in the LaxarJS Patterns library. Have a good look at all of them before coming up with your own patterns, in order to maximize the synergy of your widgets, especially when aiming for reuse.
Core Patterns
The core event patterns allow widgets to interact with the LaxarJS runtime. They are related to initialization of pages and navigation between them.
Page Lifecycle
After all widget controllers have been instantiated, the runtime publishes a beginLifecycleRequest
event.
Widgets that need to publish events on page load should do so after receiving this event, ensuring that all receivers have been set up when their events are delivered.
A will/did-response may be used by widgets to defer rendering of the page until they have been initialized, which is usually not recommended.
Before navigating away from a page, the runtime publishes the endLifecycleRequest
event.
Widgets that need to save state to a service should respond with a willEndLifecycle
event, perform their housekeeping and publish an didEndLifecycle
when done.
Event name | Payload Attribute | Description |
---|---|---|
beginLifecycleRequest.{lifecycleId} |
published by the runtime to tell widgets that publishing of events is safe now | |
lifecycleId |
the lifecycle ID (currently, this is always "default" ) |
|
willBeginLifecycle.{lifecycleId} |
published by widgets and activities to defer page rendering (not recommended) | |
lifecycleId |
see above | |
didBeginLifecycle.{lifecycleId} |
published by widgets and activities when page rendering may commence (not recommended) | |
lifecycleId |
see above | |
endLifecycleRequest.{lifecycleId} |
published by the runtime to tell widgets that the page is about to be destroyed | |
lifecycleId |
see above | |
willEndLifecycle.{lifecycleId} |
published by widgets and activities to defer tear down of the page (if necessary) | |
lifecycleId |
see above | |
didEndLifecycle.{lifecycleId} |
published by widgets and activities when page tear down may commence (after deferring it) | |
lifecycleId |
see above |
Navigation
Widgets and activities may initiate navigation using a navigateRequest.{target}
event, substituting an actual navigation target instead of the placeholder {target}
.
The event is interpreted by the LaxarJS runtime as follows:
- if target is
"_self"
, the runtime will simply propagate its place-parameters by publishing adidNavigate
event right away - if target is one of the targets configured for the current place (in the flow definition), the runtime will initiate navigation to the corresponding place
- otherwise, if target is a place within the flow definition, the runtime will initiate navigation to that place
- otherwise, nothing will happen.
When initiating navigation, the LaxarJS runtime will:
- extract any place parameters from the event payload of the request event
- publish a
willNavigate.{target}
event with the corresponding target and parameters - publish an
endLifecycle
event and wait for any respondents - perform navigation by destroying the current page and loading the page associated with the new place
- publish a
beginLifecycle
event and wait for any respondents - publish a
didNavigate.{target}
event, with the corresponding target and parameters as well as the resolved place
Here is the summary of navigation events:
Event name | Payload Attribute | Description |
---|---|---|
navigateRequest.{target} |
published by widgets and activities to indicate that a navigation has been requested | |
target |
the navigation target (used in the payload as well as in the event name) | |
data |
a map from place parameter names to parameter values | |
willNavigate.{target} |
published by the runtime to indicate that navigation has started | |
target , data |
see above | |
didNavigate.{target} |
published by the runtime to indicate that navigation has finished | |
target , data |
see above | |
place |
the actual place that was navigated to, now the current place |
More information on navigation is available in the "Flow and Places" manual.
Locales and i18n
Events related to locales are described in the "i18n" manual.
More Patterns
The patterns described so far are used mainly for widgets to interact with the LaxarJS runtime. For application patterns that help widgets to interact with each other, refer to the LaxarJS Patterns documentation.
Event Reference
Possibly the most important API provided by LaxarJS to widgets is the event bus. This section lists the exact details of using the event bus, and explains how events should be named.
The Event Bus API
The event bus is injected into widget as axEventBus
.
It is also available as the eventBus
property of the axContext
injection (or $scope
in AngularJS).
It has essential methods that allow to implement all patterns mentioned above.
subscribe( eventPattern, callback [, options] )
Creates a subscription on the event bus.
- The `eventPattern` is a prefix for events to subscribe to: Events that start with the given sequence of (sub-)topics will be handled by this subscription. For example, a subscription to the pattern `didSave` will be triggered for the event `didSave.myDocument` as well as for the event `didSave.preferences-main`. Most of the time, widgets are only interested in very specific events related to resources they work with or actions they handle, so they use patterns such as `didReplace.someResource` where `someResource` is given by the page configuration. - The `callback` is the function which will be called to process any matching events. Event subscription callbacks receive two arguments: + The `event` is this subscriber's copy of the payload, as published by the sender of the event. + The `meta` object contains additional information about the event, in particular the `sender` (identified by a string) and the `name` (under which the event was published). - The `options` are usually not required for widgets: Using `options.subscriberId`, the subscriber can identify itself to the event bus. The LaxarJS runtime decorates each widget's event bus so that this option is already set correctly.
The method subscribe
does not return a value.
publish( eventName, payload [, options ] )
Publishes an event to all interested subscribers. Delivery is asynchronous: control is returned to the caller immediately, and delivery of all scheduled events will be performed afterwards in batch. The event payload is cloned immediately so that the caller is free to modify it right after publishing. Returns a promise that is resolved after the event has been delivered to all subscribers.
- The `eventName` is used to identify matching subscribers. It is matched against the `eventPattern` of any subscriptions. - The `payload` will be delivered as the `event` parameter to any matching subscriber callbacks. It is copied right away, making it safe to modify afterwards. - The `options` are usually not required for widgets: By setting `options.deliverToSender` to `false`, widgets can ignore their own events, which can sometimes be necessary to avoid loops.
The method publish
returns a promise that is resolved after the event has been processed by all matching subscribers.
publishAndGatherReplies( requestEventName, payload [, options ] )
Publishes a request event, gathers all will-responses during delivery and then waits for all outstanding did-responses.
The parameters payload
and options
are equivalent to the regular publish
-method.
Returns a promise that is resolved when all did-responses have been received.
This information should help to get started with the event bus and intentionally omits a lot of details. For full information, refer to the EventBus API documentation.
Event Name Grammar
This is the formal grammar for event names, in EBNF:
<event-name> ::= <topic-id> [ '.' <topic-id> ]* <topic-id> ::= <sub-topic-id> [ '-' <sub-topic-id> ]* <sub-topic-id> ::= [a-z][+a-zA-Z0-9]* | [A-Z][+A-Z0-9]*