Skip to main content

Global Message Managers

In the web-api and babylon-view packages we have created some helpers for when using the History API and/or the window message API. These resources are globally shared in the browser, so special care must be taken so that they are not affected by other parts of a web site integrating Stage interfering with Stage. In addition to these we have also build a third helper using textual input. This can be used to extract configuration for external use, or, with care taken, to do direct text edits to configuration.

Most things mentioned in this document can be a bit challenging to work with. We believe that no integrator will have the need or will want to use everything. For common use cases we believe that settling with the History API helper classes will be enough to provide a good experience for your end users.

If you need to do something more challenging, please contact us and we can advise you.

Functional selection

Please note that this warning does not apply to initial configuration passed when creating a new Product.

For Product Configuration connectors, these Message Managers serialize and deserialize configurations.

Functional selection is a Catalogues feature where selecting Options on Features result in that you "jump" to another Product as a result of the Validate call. You normally do not notice that a functional selection has occurred except for the styleNr changing. Functional selection can change which Features from the original product call are used as root Features. This can in turn affect if serialized configuration can be applied or not.

The SDK can currently only apply serialized configuration if the list of root Features has not changed. For this reason, when functional selection has happened, using the Message Managers to extract data for external systems might work well, but reapplying back into Stage will probably fail.

History API

https://developer.mozilla.org/en-US/docs/Web/API/History_API

The History API allows the browser history to be manipulated. The URL can be updated, and the history stack can be changed in the browser, without any actual navigation happening.

CfgHistoryManager

This class connects Stage to the History API. It handles how the History Stack is written and read by Stage.

On it's own it does not do anything - it requires at least one connector.

Connector History Mode

The connector can be configured in 5 different modes

  • DoNotWrite: No updates to camera or Product Configuration are ever written. The connector is only used to for reading initial configurations and/or to actively request URL:s from it.
  • Replace: The History stack is updated when camera or Product Configuration change. The page will remember the current state when navigating back and forth to other pages. The URL is not updated.
  • Push: Updates to camera or Product Configuration are pushed onto the History stack. You can navigate back and forth using the web browser navigation. The URL is not updated.
  • ReplaceAndUpdateUrl: Like Replace, but the URL is updated.
  • PushAndUpdateUrl: Like Push, but the URL is updated.

CfgHistoryToCameraConfConnector

This class connects a CfgHistoryManager to the camera in Stage. It can write the current camera Yaw and Pitch to the History and/or URL. At load, if the parameters are in the URL, these will be used as the starting camera configuration.

Use this if you want copy - pasted URLs to contain camera orientation, or you want the camera orientation to be remembered when navigating back and forth in the browser, or use the Share functionality.

CfgHistoryToProdConfConnector

This class connects a CfgHistoryManager to the Product Configuration in Stage. It can write what Options and optional Additional Products are selected to the URL and/or History. What is in the URL will be used as the starting Product Configuration for the loaded Product.

Use this if you want copy - pasted URLs to contain Product Configuration, or you want the Product Configuration to be remembered when navigating back and forth in the browser, or use the Share functionality.

Too long URLs

Be aware, a complex Product can easily surpass the maximum browser URL length in certain browsers. Some browsers allow no more than about 2000 characters in an URL.

No guarantees

There are no guarantee that when navigating back through History or using an old URL the Product has not changed in such a way that the stored Product Configuration is no longer correct for the Product.

Window Message API

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

The Window Message API is designed to allow safe communication between for instance iframes in a web browser.

CfgWindowMessageManager

This class connects Stage to the Window Message API. It listens to incoming messages and filters out only those addressed to Stage. It can send messages with special formatting indicating that they are intended for Stage.

We use this in Stage Embedded to allow customers to extract data from the iframe, and to allow data to be sent into the iframe.

On it's own it does not do anything - it requires at least one connector.

CfgWindowMessageToCameraConfConnector

This class connects a CfgWindowMessageManager to the camera in Stage. It will send updates to current camera Yaw and Pitch as specially formatted messages. The sending is rate limited to not flood recipients.

This class will also listen for incoming messages, and if these are of the right type the camera Yaw and Pitch will be updated.

CfgWindowMessageToProdConfConnector

This class connects a CfgWindowMessageManager to the Product Configuration in Stage. It will send what Options and optional Additional Products are selected as specially formatted messages. It will listen for incoming messages and update the Product Configuration accordingly.

Observable State

In addition to History API and Window Message API we have created a third means of communication, the Observable State. This stores a copy of the current state in a (relatively) easy to read textual JSON format. It allows the same type of data to be sent the other direction, to update the configuration of the Product.

Extracting data this way can be useful when wanting to transfer configuration from one system running Stage to another, by Copy-Paste. Or for extracting the current state of the Product from Stage Embedded.

CfgObservableStateManager

This class connects Stage to its own Observable State.

We use this in various places, largely as a debug tool but also to ease integration work by copy-paste.

On it's own it does not do anything - it requires at least one connector.

CfgObservableStateToCameraConfConnector

This class connects a CfgObservableStateManager to the camera in Stage. It will send updates to current camera Yaw and Pitch as specially formatted messages. The sending is rate limited to not flood recipients.

This class will also listen for incoming messages, and if these are of the right type the camera Yaw and Pitch will be updated.

CfgWindowMessageToProdConfConnector

This class connects a CfgObservableStateManager to the Product Configuration in Stage. It can be configured to send in the old v1. format, or the the new v2. or both. It will listen for incoming messages and update the Product Configuration accordingly.

Example, with History API

// Based around ProductView.tsx

// ...

// To connect to the camera
const cameraControl = useMemo(() => new Observable<CfgOrbitalCameraControlProps>(), []);

// No need for a useMemo, this will always be the same instance
const historyManager = CfgHistoryManager.instance;

const [historyToProdConfConnector, setHistoryToProdConfConnector] =
useState<CfgHistoryToProdConfConnector>();

const [initialProdConf, setInitialProdConf] = useState<DtoProductConf | undefined>();

useEffect(() => {
const { instance: historyConnector, initial } = CfgHistoryToProdConfConnector.make(
historyManager,
HistoryMode.DoNotWrite
);
setHistoryToProdConfConnector(historyConnector);
setInitialProdConf(initial);
return () => {
historyConnector.destroy();
};
}, [historyManager]);

useEffect(() => {
if (historyToProdConfConnector === undefined) {
return;
}
historyToProdConfConnector.setProduct(product);
}, [historyToProdConfConnector, product]);

const cameraDefaults = useMemo(
() => ({
yaw: degToRad(110),
pitch: degToRad(70),
}),
[]
);

const [initialCameraConf, setInitialCameraConf] = useState<CfgOrbitalCameraControlProps>();

useEffect(() => {
const { initial, instance: historyConnector } = CfgHistoryToCameraConfConnector.make(
historyManager,
cameraDefaults,
cameraControl,
HistoryMode.Replace
);
setInitialCameraConf(initial);
return () => {
historyConnector.destroy();
};
}, [cameraControl, cameraDefaults, historyManager]);

const viewConfiguration = useMemo<SingleProductDefaultCameraViewConfiguration>(
() => ({
camera: {
initial: initialCameraConf, // Where the camera will start at
...cameraDefaults, // Where the camera will reset to
},
}),
[initialCameraConf, cameraDefaults]
);

//...

useEffect(() => {
//...

// initialProdConf passed
CfgProduct.make(api, prodParams, undefined, initialProdConf).then((product) => {
//...
});
}, [setError, api, loadingObservable, prodParams, resetToken, initialProdConf]);

//...

<BabylonView
//...
cameraControl={cameraControl}
configuration={viewConfiguration}
//...
/>;

Example, with Everything

Please don't do this

This example is just for reference. We don't think that anyone should install everything at the same time.

// Based around ProductView.tsx

// We use useMemo around much here, 'cause they do not work well when they are recreated frequently

// To connect to the camera
const cameraControl = useMemo(() => new Observable<CfgOrbitalCameraControlProps>(), []);

// No need for a useMemo, this will always be the same instance
const windowMessageManager = CfgWindowMessageManager.instance;

// We could optionally not pass any window at all here. Then we would never send, only listen.
// This way is also how you can add target origin and acceptable message origin. For details, see
// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage .
useEffect(() => {
windowMessageManager.remoteEnds = [someOtherProbablyIframeWindow];
// windowMessageManager.targetOrigin = "my target origin";
// windowMessageManager.acceptableMessageOrigin = "my acceptable message origin";
}, [windowMessageManager]);

// No need for a useMemo, this will always be the same instance
const historyManager = CfgHistoryManager.instance;

// No need for a useMemo, this will always be the same instance
const observableStateManager = CfgObservableStateManager.instance;

const [messageToProdConfConnector, setMessageToProdConfConnector] =
useState<CfgWindowMessageToProdConfConnector>();

useEffect(() => {
const connector = new CfgWindowMessageToProdConfConnector(
windowMessageManager,
undefined, // This argument allows you to turn off validation calls to the server when receiving messages.
CfgProdConfMessageVersions.V1dot0 | CfgProdConfMessageVersions.V2dot0 // This argument allows you to control what versions of the product configuration are sent.
);
setMessageToProdConfConnector(connector);
return () => {
connector.destroy();
};
}, [windowMessageManager]);

const [historyToProdConfConnector, setHistoryToProdConfConnector] =
useState<CfgHistoryToProdConfConnector>();

const [initialProdConf, setInitialProdConf] = useState<DtoProductConf | undefined>();

useEffect(() => {
const { instance: historyConnector, initial } = CfgHistoryToProdConfConnector.make(
historyManager,
HistoryMode.Push, // So that we can navigate back and forth in between configurations we have selected.
// Use HistoryMode.PushAndUpdateUrl if you also want to browser to update its URL.
// Use HistoryMode.DoNotWrite if you just want to use the connector for initial
// configuration (share)
"optionalQsKeyOverride", // optional, if you want the query string key to be something else than normal. Leaving the empty is recommended, as it will make copy-paste between web sites easier
true // Use validation calls or not when navigating. If you do it means you assume the risk of the Product being update mid-browsing-session is low.
);
setHistoryToProdConfConnector(connector);
setInitialProdConf(initial);

return () => {
connector.destroy();
};
}, [historyManager]);

const [observableStateToProdConfConnector, setObservableStateToProdConfConnector] =
useState<CfgObservableStateToProdConfConnector>();

useEffect(() => {
const connector = new CfgObservableStateToProdConfConnector(
observableStateManager,
true, // Use validation calls or not when navigating.
CfgProdConfMessageVersions.V1dot0 | CfgProdConfMessageVersions.V2dot0, // This argument allows you to control what versions of the product configuration are sent.
true, // Include extended data which is not strictly part of the configuration, such as GroupCode.
true // Include product parameters when sending. These are always ignored when receiving.
);
setObservableStateToProdConfConnector(connector);
return () => {
connector.destroy();
};
}, [observableStateManager]);

useEffect(() => {
if (
historyToProdConfConnector === undefined ||
messageToProdConfConnector === undefined ||
observableStateToProdConfConnector === undefined
) {
return;
}
(async () => {
// It is important that history is done first, and that they wait for each other
await historyToProdConfConnector.setProduct(product);
await messageToProdConfConnector.setProduct(product);
await observableStateToProdConfConnector.setProduct(product);
})();
}, [
historyToProdConfConnector,
messageToProdConfConnector,
observableStateToProdConfConnector,
product,
]);

// Defaults for the camera when nothing in the query string is overriding
const cameraDefaults = useMemo(
() => ({
yaw: degToRad(110),
pitch: degToRad(70),
}),
[]
);

const [initialCameraConf, setInitialCameraConf] = useState<CfgOrbitalCameraControlProps>();

useEffect(() => {
// Initializes, and figures out the starting camera configuration based on cameraDefaults and the query string
const { initial, instance: historyConnector } = CfgHistoryToCameraConfConnector.make(
historyManager,
cameraDefaults,
cameraControl,
HistoryMode.Replace // So that the browser remember the latest camera position
);
const windowMessageConnector = new CfgWindowMessageToCameraConfConnector(
windowMessageManager,
initial,
cameraControl
);
const observableStateConnector = new CfgObservableStateToCameraConfConnector(
observableStateManager,
initial,
cameraControl
);
setInitialCameraConf(initial);
return () => {
historyConnector.destroy();
windowMessageConnector.destroy();
observableStateConnector.destroy();
};
}, [cameraControl, cameraDefaults, historyManager, observableStateManager, windowMessageManager]);

const viewConfiguration = useMemo<SingleProductDefaultCameraViewConfiguration>(
() => ({
initial: initialCameraConf, // Where the camera will start at
...cameraDefaults, // Where the camera will reset to
}),
[initialCameraConf, cameraDefaults]
);

//...

useEffect(() => {
//...

// initialProdConf passed
CfgProduct.make(api, prodParams, undefined, initialProdConf).then((product) => {
//...
});
}, [setError, api, loadingObservable, prodParams, resetToken, initialProdConf]);

//...

<BabylonView
//...
cameraControl={cameraControl}
configuration={viewConfiguration}
//...
/>;

//...

<TextualConfigurationView active={true} observableStateManager={observableStateManager}>
<h3>Current Configuration</h3>
</TextualConfigurationView>;