How to setup your website for embedding
Introduction
This guide outlines how a Packhunt solution can be embedded into your own website. In this guide we will be using the solution created in the Create viewer solution guide.
Before you start
Make sure to have Requested a Packhunt app endpoint before starting this guide
In this guide we will use the endpoint: https://example.packhunt.io/embed
1. Add an iframe
to your website
First add the iframe to your website’s HTML. As a src
with the embedding endpoint received in request Packhunt app endpoint. When you are done your HTML should look something like:
<!DOCTYPE html>
<html>
<head></head>
<body>
<iframe id="packhunt" src="https://example.packhunt.io/embed" frameborder="0">
<p>Your browser does not support iframes.</p>
</iframe>
</body>
</html>
With this you can listen to the load
event of the iframe to start posting messages and listening for events. See the topics below for details on how to set this up.
const iframe = document.getElementById("packhunt");
iframe.addEventListener('load', () => {
// in this context you may use iframe.contentWindow.postMessage(...)
});
⚠️ On using the sandbox directive
It is generally considered a good practice to sandbox iframe content. When doings so keep in mind that Packhunt requires at least the following allowances to function properly:
allow-same-origin
Allows the page to manage authentication cookies and load resources from the Packhunt CDN without CORS origin errorsallow-scripts
Allows the page to run scripts (but not create pop-up windows). If this keyword is not used, this operation is not allowed.allow-forms
Allows the page to submit forms. If this keyword is not used, a form will be displayed as normal, but submitting it will not trigger input validation, send data to a web server, or close a dialog.
See also: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox
2. Listen for app events
To receive messages listen to the message
event on the window object (see: MessageEvent
). The message will contain an origin
and data
property which can be used to check the source of the message and its payload respectively. Below is a snippet on how to check for the origin and unpack the message data.
The data structure contained in data
will follow the types outlined in AppEvent
.
In the next steps we will write additional handlers for the different event types that may be received by the app.
iframe.addEventListener('load', () => {
window.addEventListener(
'message',
({ origin, data }) => {
// Always check the origin of incoming messages
if (origin !== "https://example.packhunt.io") return;
// Do stuff with the received data structure
const { eventType, eventData } = data;
// TODO add handlers!
},
false
);
});
⚠️ On using message event listener
When receiving messages, always verify the sender’s identity using the origin
and possibly source
properties of the message receive. Any window (including, for example, http://evil.example.com
) can send a message to any other window within the iframe hierarchy from top to every iframe below of the current document.
See also: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns
3. Load the Packhunt app
Before you can start sending and receiving messages you need to register your website to the app. Use the LoadAppMessage
for this. The snippet below shows an example of this using the embedded viewer solution created in the previous how-to guide.
const iframe = document.getElementById("packhunt");
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage({
eventType: 'loadApp',
eventData: {
auth: {
type: "apikey",
value: "{YOUR_API_KEY}"
},
navigate: {
project: "embedded-viewer"
}
}
}, 'https://example.packhunt.io');
});
Here {YOUR_API_KEY}
should refer to a Packhunt API key that has view access to the solution.
To confirm the app was loaded succesfully await the AppLoadedEvent
. To keep an overview of the different event handlers we will introduce a handler onAppLoaded(..)
which receives the app loaded event data:
const onAppLoaded = ({status, message}) => {
if (status === "success") {
console.log("✅ Packhunt app loaded successfully!");
} else {
console.error(`Packhunt app could not be loaded got '${status}', received message: ${message}`);
}
}
iframe.addEventListener('load', () => {
window.addEventListener(
'message',
({ origin, data }) => {
// Always check the origin of incoming messages
if (origin !== "https://example.packhunt.io") return;
const { eventType, eventData } = data;
if (eventType === 'appLoaded') {
onAppLoaded(eventData);
}
},
false
);
});
When loaded succesfully you will see the following message in your browser console:
✅ Packhunt app loaded successfully!
4. Receive the embed documentation
Next it is important to verify what user defined events and messages are available. This can be done by awaiting the EmbedLoadedEvent
. For this again we will introduce a handler: onEmbedLoaded(..)
which will simply log the configuration received:
let embedName;
const onEmbedLoaded = ({status, name, message, allowedMessages, availableEvents, allowedOrigins}) => {
embedName = name; // store for later
if (status === "success") {
console.log("🚀 Packhunt embed loaded successfully!");
} else {
console.error(`Packhunt app embed app could not be loaded got '${status}', received message: ${message}`);
}
console.log("Received app configuration:", {name, allowedOrigins, allowedMessages, availableEvents})
}
iframe.addEventListener('load', () => {
window.addEventListener(
'message',
({ origin, data }) => {
// Always check the origin of incoming messages
if (origin !== "https://example.packhunt.io") return;
const { eventType, eventData } = data;
if (eventType === 'appLoaded') {
handleAppLoaded(eventData)
} else if (eventType === 'embedLoaded' ) {
onEmbedLoaded(eventData)
}
},
false
);
});
When loaded succesfully you will see the following message in your browser console:
🚀 Packhunt embed loaded successfully!
Received app configuration:
Object { name: "__root-frame_embed", allowedOrigins: (3) […], allowedMessages: {…}, availableEvents: {…} }
You will notice the sections allowedMessages
and availableEvents
which can be used to determine the shape of the userMessage
and userEvent
payloads you may send or receive from the solution. Furthermore the name
should be used later when sending userMessage
events to the solution, we will store this in a variable: embedName
.
In our embedded viewer example we will receive the following payload:
{
"name": "__root-frame_embed",
"allowedOrigins": [
"http://localhost:1313",
"https://docs.packhunt-test.io",
"https://docs.packhunt.io"
],
"allowedMessages": {
"set-inputs": {
"description": "Set all inputs",
"schema": {
"type": "object",
"required": [
"my-ground",
"my-length",
"my-width"
],
"additionalProperties": false,
"properties": {
"my-ground": {
"type": "boolean",
"description": "Show or hide the ground plane."
},
"my-length": {
"type": "number",
"description": "Set the box length."
},
"my-width": {
"type": "number",
"description": "Set the box width.",
"enum": [
200,
400,
600
]
}
}
}
}
},
"availableEvents": {
"model-warnings": {
"description": "User warnings from the geometry model.",
"schema": {
"type": "object",
"required": [
"my-length-warning"
],
"additionalProperties": false,
"properties": {
"my-length-warning": {
"type": [
"string",
"null"
],
"description": "This warning fires when the length is larger than twice the width."
}
}
}
},
"__root-frame_my-geometry-model-status": {
"description": "This event is fired when the status of this model is changed",
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"unset",
"computing",
"success",
"error"
]
}
}
}
}
}
}
5. Add your user inputs
From the previous step we can see that the message set-inputs
is allowed. Based on the schema defined for this message we can see we need:
- A number input for
my-length
- A number input for
my-width
limited to options:200
,400
and600
- A boolean input for
my-ground
A simple form to capture these inputs could look something like this:
<form id="example-form" class="card card-body" style="max-width:300px;">
<div class="form-group">
<label for="my-length">Length: </label>
<input type="number" min=0 max=2000 value=200 class="form-control" id="my-length" aria-describedby="my-length-help">
<small id="my-length-help" class="form-text text-muted">Set a length between 0 and 2000mm</small>
</div>
<div class="form-group">
<label for="my-width">Width:</label>
<select class="form-control" id="my-width" aria-describedby="my-width-help">
<option value=200>200mm</option>
<option value=400>400mm</option>
<option value=600>600mm</option>
</select>
<small id="my-width-help" class="form-text text-muted">Please select a width</small>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="my-ground" checked>
<label class="form-check-label" for="my-ground">Toggle ground plane</label>
</div>
<button type="submit" class="btn btn-primary">Update model</button>
</form>
6. Send a user message
With the form in place we can send an AppUserMessage
for the message with messageId
set-inputs
. We do this by listening to the form’s submit
event. On the submit callback we then read the form inputs and post the message. For our example this will look something like:
const form = document.getElementById("example-form");
let embedName; // set in onEmbedLoaded handler
const sendSetInputs = (name) => {
const messagePayload = {
['my-length']: Number(document.getElementById('my-length').value.trim()),
['my-width']: Number(document.getElementById('my-width').value.trim()),
['my-ground']: document.getElementById('my-ground').checked
}
iframe.contentWindow.postMessage(
{
messageType: "userMessage",
messageData: { name, messageId: 'set-inputs', messagePayload },
},
"https://example.packhunt.io"
);
}
form.addEventListener('submit', (event) => {
event.preventDefault();
if (embedName) sendSetInputs(embedName);
}, false);
7. Receive a user event
Just like with the allowedMessages
we may listen of availableEvents
listen in the EmbedLoadedEvent
eventData from step 4. We see that in the example solution there are 2 events we can listen to:
model-warnings
: User warnings from the geometry model. Which is an object containing a string propertymy-length-warning
__root-frame_my-geometry-model-status
: This event is fired when the status of this model is changed. Which is an object containing a status
Just like with the other events let’s add a handler onUserEvent(..)
to handle these:
const onUserEvent = ({eventId, eventPayload}) => {
if (eventId === '__root-frame_my-geometry-model-status') {
console.log("Model status updated:", eventPayload.status)
} else if (eventId === 'model-warnings') {
console.log("Received model warnings:", eventPayload['my-length-warning'])
} else {
console.warn(`Unknown userEvent with id '${eventId}' with payload:`, eventPayload);
}
}
iframe.addEventListener('load', () => {
window.addEventListener(
'message',
({ origin, data }) => {
// Always check the origin of incoming messages
if (origin !== "https://example.packhunt.io") return;
const { eventType, eventData } = data;
if (eventType === 'appLoaded') {
handleAppLoaded(eventData)
} else if (eventType === 'embedLoaded' ) {
handleEmbedLoaded(eventData)
} else if (eventType === 'userEvent') {
onUserEvent(eventData);
}
},
false
);
});
When we inspect the browser console we indeed see the messages coming in when we submit our form:
Model status updated: unset
Model status updated: computing
Model status updated: success
Received model warnings: undefined
Result
The end result is your website with its own design and controls and an iframe window containing the Packhunt viewer solution. You can play around with the result below to see this all coming together.
Keep in mind that depending on the solution that is embedded different events and messages can be expected.