Embedding
Adding a konfig to a website is relatively simple via means of an iFrame. Simply embedding the iFrame renders the configurator. Handling communication between the configurator and your website is done via means of PostMessage. Below is a small snipped that should work out of the box. We'll go into the details afterwards.
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta content="utf-8" http-equiv="encoding" />
<style>
iframe {
width: 100vw;
height: 50vh;
}
p#currentState {
word-wrap: break-word;
}
</style>
</head>
<body>
<div>
<iframe
id="konfig-iframe"
src="http://localhost:3001/1c23264d-23c0-428a-9b9c-6afc3bf2a2d7"
frameborder="0"
></iframe>
</div>
<div style="width:40%;display:inline-block;">
<h1>Step</h1>
<p>Step</p>
<form id="step">
<label>
Step:
<input type="number" />
</label>
<input type="submit" title="set step" />
</form>
</div>
<div style="width:40%;display:inline-block;">
<h1>Option</h1>
<p>Set Option</p>
<form id="option">
<label>
Step: <input type="number" />
</label>
<br />
<label>
Option ID: <input />
</label>
<br />
<label>
Replacement ID: <input />
</label>
<br />
<input type="submit" title="update option" />
</form>
</div>
<h1>Konfig</h1>
<p>Konfig</p>
<p id="konfig">None</p>
<h1>State</h1>
<p>Current State:</p>
<p id="currentState">None</p>
<form id="updateState">
<label>
Update:
<input />
</label>
<input type="submit" title="update" />
</form>
</body>
<script>
(async function () {
/* --- Helpers from Konfig --- */
const frame = document.getElementById("konfig-iframe");
const dispatch = (message) => frame.contentWindow.postMessage(message, "*");
const handleMessage = async (event) => {
if (!event || !Array.isArray(event.data)) return;
const [action, data] = event.data;
switch (action) {
case "DataLoaded":
return handleDataLoaded(data);
case "AddToCart":
return handleAddToCart(data);
case "ConfigurationChanged":
return handleConfigurationChanged(data);
case "ModelsLoaded":
return console.log("Models Loaded In: ", data)
case "Screenshot":
return handleScreenshotTaken(data);
default:
console.log(action);
console.warn("Action not supported");
}
};
window.addEventListener("message", handleMessage, false);
/* --- Messages that can be send back to Konfig --- */
const messages = {
addToCartSuccess: () => dispatch(["AddToCartSuccess"]),
addToCartSuccessRedirecting: () =>
dispatch(["AddToCartSuccessRedirecting"]),
addToCartError: (e) => dispatch(["AddToCartError", e]),
applyConfiguration: (e) => dispatch(["ApplyConfiguration", e]),
setStep: (stepNo) => dispatch(["SetStep", stepNo]),
activatePreset: (stepNo, presetId) => dispatch(["ActivatePreset", stepNo, presetId]),
activateOption: (stepNo, optionId, replacementId) => dispatch(["ActivateOption", stepNo, optionId, replacementId]),
updateNumberOption: (stepNo, optionId, no) => dispatch(["UpdateNumberOption", stepNo, optionId, no]),
requestScreenshot: (id) => dispatch(["RequestScreenshot", id]),
};
/* --- Implement these --- */
function handleDataLoaded(data) {
const current = document.getElementById("konfig");
current.innerText = JSON.stringify(data, null, 2);
console.log(data);
}
function handleAddToCart(data) {
try {
console.log(data);
messages.addToCartSuccess();
} catch (e) {
console.warn("Add to Cart Error", e);
messages.addToCartError(e);
}
}
function handleConfigurationChanged(data) {
try {
const current = document.getElementById("currentState");
current.innerText = data;
} catch (e) {
console.warn("Handle Configuration Changed Error", e);
}
}
const form = document.getElementById("updateState");
form.onsubmit = (e) => {
e.preventDefault();
messages.applyConfiguration(e.target[0].value);
};
const form02 = document.getElementById("step");
form02.onsubmit = (e) => {
e.preventDefault();
messages.setStep(Number(e.target[0].value));
};
const form03 = document.getElementById("option");
form03.onsubmit = (e) => {
e.preventDefault();
let stepNo = Number(e.target[0].value);
let optionId = e.target[1].value;
let replacementId = e.target[2].value;
messages.activateOption(stepNo, optionId, replacementId);
};
let latestId = "0";
const screenshotButton = document.getElementById("screenshotButton");
screenshotButton.addEventListener("click", () => {
latestId = Date.now().toString();
messages.requestScreenshot(latestId);
console.log("screenshot")
})
const screenshotImage = document.getElementById("screenshotImage");
function handleScreenshotTaken(data) {
console.log(data)
const [id, image] = data;
if (id === latestId) {
screenshotImage.src = image;
}
}
})();
</script>
</html>
iFrame
<iframe id="konfig-iframe" src="link-to-your-konfig" frameborder="0"></iframe>
The following snipped is used to actually embed a konfig. You are free to edit
everything, but whenever updating the id
, it should be updated in the
<script />
tag as well.
Helpers
/* --- Helpers from Konfig --- */
const frame = document.getElementById("konfig-iframe");
const dispatch = (message) => frame.contentWindow.postMessage(message, "*");
const handleMessage = async (event) => {
if (!event || !Array.isArray(event.data)) return;
const [action, data] = event.data;
switch (action) {
switch (action) {
case "DataLoaded":
return handleDataLoaded(data);
case "AddToCart":
return handleAddToCart(data);
case "ConfigurationChanged":
return handleConfigurationChanged(data);
case "ModelsLoaded":
return handleModelsLoaded(data);
default:
console.log(action);
console.warn("Action not supported");
}
};
window.addEventListener("message", handleMessage, false);
Our helpers are simply a bit of boilerplate that we set-up to make sure we can
communicate with the configurator. The frame
variable holds a reference to the
iFrame, such that we can send messages to said iFrame. dispatch
is a function
that sends an actual message. handleMessage
is where things get a bit more
interesting. We use ReasonML in our Frontends,
and we're able to send a typed message. As such, we know exactly which messages
our configurator can send. Currently, there are two. We'll get into some more details
in the next section.
Finally, we setup a listener that actually listens for events coming from the
configurator.
Messages
/* --- Messages that can be send back to Konfig --- */
const messages = {
/* addToCartSuccess hides the loader */
addToCartSuccess: () => dispatch(["AddToCartSuccess"]),
/* addToCartSuccessRedirecting changes the loading message to show 'redirecting' */
addToCartSuccessRedirecting: () => dispatch(["AddToCartSuccessRedirecting"]),
/* addToCartError hides the loader - error handling will be implemented later */
addToCartError: (e) => dispatch(["AddToCartError", e]),
/* Takes one of the configuration outputs from the 'ConfigurationChanged' events and applies it */
applyConfiguration: (e) => dispatch(["ApplyConfiguration", e]),
/* Takes a number, and updates the step -- Steps start at 0 */
setStep: (stepNo) => dispatch(["SetStep", stepNo]),
/* Activates a Preset */
activatePreset: (stepNo, presetId) => dispatch(["ActivatePreset", stepNo, presetId]),
/* Activates an option */
activateOption: (stepNo, optionId, replacementId) => dispatch(["ActivateOption", stepNo, optionId, replacementId]),
/* Updates an option that holds a number */
updateNumberOption: (stepNo, optionId, no) => dispatch(["UpdateNumberOption", stepNo, optionId, no]),
/* Requests a screenshot. The ID is a string and is propagated to the response, such that multiple screenshots can be requested and matched. */
requestScreenshot: (id) => dispatch(["RequestScreenshot", id]),
};
Next to knowing which messages the Viewer can send, we also know which messages it can receive. To make sure there are no typo's there from your side, we add them to an object lookup.
Implementation
/* --- Implement these --- */
function handleDataLoaded(data) {
const current = document.getElementById("konfig");
current.innerText = JSON.stringify(data, null, 2);
console.log(data);
}
function handleModelsLoaded(data) {
console.log(data);
}
function handleAddToCart(data) {
try {
console.log(data);
messages.addToCartSuccess();
} catch (e) {
console.warn("Add to Cart Error", e);
messages.addToCartError(e);
}
}
function handleConfigurationChanged(data) {
try {
const current = document.getElementById("currentState");
current.innerText = data;
} catch (e) {
console.warn("Handle Configuration Changed Error", e);
}
}
const form = document.getElementById("updateState");
form.onsubmit = (e) => {
e.preventDefault();
messages.applyConfiguration(e.target[0].value);
};
const form02 = document.getElementById("step");
form02.onsubmit = (e) => {
e.preventDefault();
messages.setStep(Number(e.target[0].value));
};
const form03 = document.getElementById("option");
form03.onsubmit = (e) => {
e.preventDefault();
let stepNo = Number(e.target[0].value);
let optionId = e.target[1].value;
let replacementId = e.target[2].value;
messages.activateOption(stepNo, optionId, replacementId);
};
let latestId = "0";
const screenshotButton = document.getElementById("screenshotButton");
screenshotButton.addEventListener("click", () => {
latestId = Date.now().toString();
messages.requestScreenshot(latestId);
console.log("screenshot")
})
const screenshotImage = document.getElementById("screenshotImage");
function handleScreenshotTaken(data) {
console.log(data)
const [id, image] = data;
if (id === latestId) {
screenshotImage.src = image;
}
}
On Configuration Loaded
To make use of this, use the handleDataLoaded
function.
On Underlying 3d Models Loaded
To make use of this, use the handleModelsLoaded
function. The data here is
the loadtime in millisecond.
Add To Cart
To make use of this, use the handleAddToCart
function. The data that is sent
a long looks like this:
type optionData = {
name: string,
additionalPrice: Number | null,
sku: string | null,
screenshot: string | null, /* Image data as png base64EncodedString */
optionType: "MATERIAL" | "GROUP" | "OBJECT" | "NUMBER",
value: string // In case optionType is Number, this semantically means "number"
};
type stepData = {
name: string,
screenshot: string | null, /* Image data as png base64EncodedString */
options: [optionData],
};
type addToCartData = {
name: string,
currencySymbol: string | null,
currencyCode: string | null,
price: number | null,
steps: [stepData],
screenshot: string | null, /* Image data as png base64EncodedString */
state: string /* Encoded format for the current configuration, to be passed back in 'ApplyConfiguration' */
};
Configuration Changed
The configuration can be programmatically changed from the outside using the 'applyConfiguration' event. Input for that can be taken either from the 'ConfigurationChanged' event, or from the state
in the addToCartData
object. It is a base64 encoded string representing our internal representation.
Forms
There are three forms in this example. The first form updates the state of the application. You can take the changed configuration string, and push that into the configurator. The second (#form02
) takes a number, and pushes it into the setStep
. It's important to note that HTML input
elements output a string, regarless of their type
.
(#form03
) takes the stepNo
, optionId
and replacementId
, and activates an option.
stepNo
-> This is the index of the step the option resides inoptionId
-> This the the ID of the optionreplacementId
-> This the the exact replacement you want to activate. NOTE - this is NOT thereplacementDetailsId
. FindreplacementId
to see all of them.
The screenshot button requests a screenshot and subsequently, there is some code that handles the response. The requestId
has to be given, and is subsequently propagated back through the screenShotTaken
event.