Experience Manager SPA API
Introduction
Integrating your SPA with the Experience manager enables dynamic management of parts of your SPA. If you integrate your SPA with the Experience manager, CMS webmasters can customize, add, move, and remove components of your SPA, just like they can (and may be used to) for non-SPA pages. Such manageable components must be known to HST, and HST exposes them through the Delivery API to the SPA, which must be aware of the concept of components, as well as of the specific set of components in use by / available to the SPA.
Experience manager - SPA integration is optional. While it is possible to only integrate your SPA with Bloomreach Experience Manager's Delivery API, you then miss the ability to let CMS users manage the components exposed by the Delivery API.
Conceptually, the integration between your SPA and the Experience manager is bi-directional: If a CMS user changes parts of the Delivery, the SPA must be notified, and if the visitor of the (preview) page, displayed in the Experience manager, changes the state/route of the SPA, the Experience manager must be notified. In both scenarios, the goal is that your SPA and the Experience manager stay in sync.
API
The integration API between your SPA and the Experience manager consists of three parts, described in separate sub-sections:
JavaScript
PostMessage-based Remote Procedure Call (RPC)
All the communication between an SPA and the Experience manager relies on the postMessage API. The API provides an event-based communication between the SPA context and the brXM context, which works even with the Cross-Origin resources.
Since the API is based on the event model, there is no way to track the progress of processing one particular message. Because of that, all the two-communication with the Experience manager should be fully asynchronous. In that case, the SPA should send a message with a unique identifier so the brXM will reuse the identifier in the response message. The message identifier will be used by the SPA to determine the result of the remote call. The following communication should work both ways when the SPA responds to some of the requests coming from brXM.
Security Concerns
The browser does not limit a message origin for an SPA. That means that the SPA should always verify the origin of the message. In the SPA SDK, the event origin should always strictly match with the origin determined from the cmsBaseUrl configuration option. On the other side, brXM requires the origin to match with the origin from the org.hippoecm.hst.configuration.channel.PreviewURLChannelInfo_url property.
The same concerns are taken into account for the targetOrigin parameter. The SPA SDK as well as brXM use the cmsBaseUrl and org.hippoecm.hst.configuration.channel.PreviewURLChannelInfo_url accordingly to limit the recipients of an outgoing message.
Events
Some of the messages do not require an acknowldgement or a result from the remote side. Such messages are of the event type and do not contain any identifying data in the payload.
So those messages can be sent like in the following example:
window.parent.postMessage( { type: 'brxm:event', event: 'something', payload: { some: 'data' } }, 'http://example.com', );
In the example above, the event message object consists of the following properties:
- type - message type should always be brxm:event for the events.
- event - the event name.
- payload - the event parameters which will be serialized into a plain object by the browser before sending.
Requests
This type of message requires a response from the remote end and represents remote procedures. On the other side those procedures always have a 1 to 1 connection with some code or a function. The message payload must contain a unique identifier so the caller can identify the response.
const pool = new Map(); function callSomething(...payload) { return new Promise((resolve, reject) => { const id = Math.random(); pool.set(id, [resolve, reject]); window.parent.postMessage( { id, payload, type: 'brxm:request', command: 'something', }, 'http://example.com', ); }); }
In the example above, the event message object consists of the following properties:
- type - message type should always be brxm:request for the remote calls.
- command - the remote procedure name.
- id - the remote call identifier.
- payload - the remote procedure parameters array, which will be passed to the remote function.
Responses
This type of message contains a result or a thrown error of the remote call. The message should include an identifier received in the request. The following code shows handling messages on the SPA side:
const pool = new Map(); window.addEventListener('message', async (event) => { if (event.origin !== 'http://example.com') { return; } switch (event.data && event.data.type) { case 'brxm:request': return processRequest(event.data); case 'brxm:response': return processResponse(pool, event.data); } }); async function processRequest(request) { try { const result = await window[request.command](...request.payload); window.parent.postMessage( { result, id: request.id, state: 'fulfilled', type: 'brxm:response', }, 'http://example.com', ); } catch (error) { window.parent.postMessage( { result, id: request.id, state: 'rejected', type: 'brxm:response', }, 'http://example.com', ); } } function processResponse(pool, response) { if (!pool.has(response.id)) { return; } const [resolve, reject] = pool.get(response.id); pool.delete(response.id); if (response.state === 'rejected') { reject(response.result); return; } resolve(response.resolve); }
In the example above, the event message object consists of the following properties:
- type - message type should always be brxm:response for the responses.
- state - the call state. It can be either fulfilled or rejected.
- id - the remote call identifier.
- payload - the remote call result or a thrown exception.
Initial Rendering
The initial rendering flow is described in Image 1. The initialization consists of three steps:
- Notify brXM that the integration part is ready.
- Inject the requested brXM JavaScript asset.
- Synchronize all the overlays after the rendering.
Image 1. Initial rendering
Component Update
The component update flow is described in Image 2. The update consists of two steps:
- Notify the SPA about the component update.
- Synchronize all the overlays after the rendering.
Image 2. Component update
Reference
Origin | Type | Name | Parameters | Description |
---|---|---|---|---|
SPA | Event | ready | none | The event is being triggered by the SPA when the integration layer is ready. |
SPA | Procedure | inject |
|
The remote procedure is requesting a JavaScript asset injection. The injected JavaScript asset initializes the Experience manager user interface. |
brXM | Event | update |
|
The event is being triggered by brXM whenever the component updates via the Experience manager user interface. This event should trigger component rerendering on the SPA side using properties passed in the event payload. |
brXM | Procedure | sync | none* | The remote procedure is initiating the Experience manager user interface synchronization. The procedure should be called after the initial rendering and after every component rerendering. This should synchronize the positions and sizes of the Experience manager controls. |
* When no parameters are required, provide an empty array as payload ("[ ]")
JavaScript Integration prior to brXM 14.2
The Javascript part of the API covers both directions of the Experience manager - SPA integration. For this part of the API, the Experience manager requires the SPA to have constructed the following, page-global object:
window.SPA = { init: (cms) => { // Remember the cms object for subsequent callbacks }, renderComponent: (id, propertiesMap) => { // Implement logic for re-rendering the component with the provided ID, // given a map of component properties } };
By means of this SPA object, your SPA declares to the Experience manager how the Experience manager can communicate with your SPA. The Experience manager listens to the load event of the preview page, then checks if the new page defines the SPA object, thereby declaring that it is an "SPA page". If the SPA object is found, the Experience manager immediately calls the init function to register a cms object, through which the SPA can call back the Experience manager. The Experience manager does not expect any return value from the init function. The structure of the cms object is described below. First, let's discuss the renderComponent function.
If the Experience manager has detected that the current preview page is an "SPA page", most component related operations (i.e. add, move, delete) trigger a clean reload of the page in order to bring your SPA in sync with the changed page model. For the case of editing the properties of a component, this wouldn't work, because the Experience manager hasn't yet persisted the changes to the component properties to the back-end, and the page model wouldn't be able to provide these changes to the SPA in case of a reload. In order to facilitate showing the effect of the changed properties to the CMS user, the SPA object must therefore implement the renderComponent function. When a CMS user is editing the properties of a component, the Experience manager calls this function, providing the SPA with the ID of the component and a map of properties, which should be used to re-render said component. The SPA must take these properties and forward them to the Delivery API, requesting an updated model for just that component, and then update its internal state and DOM accordingly. The renderComponent function may return false to trigger the normal component rendering logic (which replaces the DOM of the component). Any other return value (we recommend true by default) instructs the Experience manager to take no further action with respect to re-rendering the component. Re-rendering a component may result in a need for the Experience manager to sync its component overlay to the updated page. The next paragraph describes how this works.
During page initialization, the Experience manager registers a cms object with the SPA, by calling the init function. The cms object exposes the following functions:
{ sync: () => { // Call this function when the set, ordering of components, or their dimensions // on the page has changed } }
The sync function must be called by the SPA to trigger the synchronization with the Experience manager. This function takes no arguments and doesn't produce a meaningful return value. When triggered, the Experience manager analyses the DOM to take further action. See the DOM section below for more details.
The SPA must call the function when the set or order of components currently displayed has changed. Most prominently, this is the case when the page is fully drawn initially, based on the retrieved page model. Also, if the visitor of the preview page navigates to a different state / route of the SPA, which displays a different set (or order) of components, the sync trigger must be called as well.
The SPA must also call the sync function when the dimensions of one or more components currently displayed on the page have changed. Most prominently, this is the case when (the chain of events triggered by) a renderComponent call from the Experience manager (i.e. the editing of component properties) has completed in the SPA. The Experience manager then makes sure that its component overlay matches the components currently displayed on the page.
The Experience manager monitors the DOM of the preview page for changes, and already syncs the overlay for every DOM change it sees. The SPA therefore only needs to call the sync function itself when the dimensions of components change without a DOM change. An example is loading an image without explicit dimensions: once the image is loaded, the layout of the page probably changes yet without any changes to the DOM.
DOM
The DOM part of the API is needed so that the SPA can tell the Experience manager where in the page DOM it can find which component / container. When running the enterprise version of Bloomreach Experience Manager, the page model includes additional metadata in the Experience manager preview version of the Delivery API which the SPA must push into the page DOM (at the appropriate locations), such that the Experience manager can detect and deal with components and containers. This metadata has the form of HTML comments and they are used to wrap the start and end DOM location of a component or container: The SPA must transparently insert the start comment (<component-model>._meta.beginNodeSpan[0].data) before the DOM element representing the component or container, and the end comment (<component-model>._meta.endNodeSpan[0].data) after the DOM element representing the component or container.
On top of the component and container markers, the SPA must also insert potentially multiple pieces of meta-data at the page level. This data is, again, made available as meta-data in the page model, and the SPA can access it at <page-model>.page._meta.endNodeSpan[n].data, where n iterates over the entries in the endNodeSpan list.
Delivery API
To retrieve the preview version of the Delivery API, the SPA must use JSON Web Token (JWT) authentication. If the SPA initial HTML is served by the HST by using the SpaSitePipeline, then this is automatically the case. By default, the Delivery API Preview is available at
http://{cms-host}/site/resourceapi
where the resourceapi is configurable via the hst:pagemodelapi property.
The SPA must include a JSON Web Token in the Authorization header using the Bearer schema. For example:
Authorization: Bearer xxxxx.yyyyy.zzzzz
The token can be gathered from the token query string parameter. The parameter will be appended to the SPA URL:
http://localhost:3000/?token=xxxxx.yyyyy.zzzzz
When having a valid authorization token, you can access the above Delivery API Preview resulting in an output that
- Shows the preview of the content.
- Includes a beginNodeSpan and endNodeSpan elements.
Then when, for example, accessing
http://localhost:8080/site/resourceapi/blog/2018/06/first-blog-post.html
the Delivery API Preview response for a single HstComponent looks something like:
{ "id": "r6_r1_r2_r1", "name": "content", "componentClass": "org.onehippo.cms7.essentials.components.EssentialsContentComponent", "type": "CONTAINER_ITEM_COMPONENT", "label": "Blog Detail", "models": { "document": { "$ref": "\/content\/ua95a21d480194f1abe1181c532775102" } }, "_meta": { "params": { }, "beginNodeSpan": [ { "type": "comment", "data": "<!-- { \"HST-Label\":\"Blog Detail\", \"HST-LastModified\":\"1528271870520\", \"HST-XType\":\"hst.item\", \"uuid\":\"c5d11a29-307b-4ad4-b3a8-e0799f94789a\", \"HST-Type\":\"CONTAINER_ITEM_COMPONENT\", \"refNS\":\"r6_r1_r2_r1\", \"url\":\"\/site\/resourceapi\/blog\/2018\/06\/first-blog-post.html?_hn:type=component-rendering&_hn:ref=r6_r1_r2_r1\"} -->" } ], "endNodeSpan": [ { "type": "comment", "data": "<!-- { \"uuid\":\"c5d11a29-307b-4ad4-b3a8-e0799f94789a\", \"HST-End\":\"true\"} -->" } ] }, "_links": { "componentRendering": { "href": "\/site\/resourceapi\/blog\/2018\/06\/first-blog-post.html?_hn:type=component-rendering&_hn:ref=r6_r1_r2_r1" } } }
The SPA code must place the beginNodeSpan and endNodeSpan around the component's HTML.
Delivery API Integration prior to brXM 14.2
To retrieve the preview version of the Delivery API, the SPA must be served over the same host as the CMS runs (for SSO purposes). If the SPA initial HTML is served by the HST by using the SpaSitePipeline, then this is automatically the case. By default, the Delivery API Preview is available at
http://{cms-host}/site/_cmsinternal/resourceapi
where the _cmsinternal is the default but configurable and resourceapi is also configurable via the hst:pagemodelapi property. When having an SSO between the CMS webapp and the site webapp (e.g. by logging in into the CMS and opening the SPA channel), you can access the above Delivery API Preview.