How to setup your website for embedding

A how-to guide to setup a simple embedding website

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.

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(...)
});

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
  );
});  

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 and 600
  • 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>
Set a length between 0 and 2000mm
Please select a width

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:

  1. model-warnings: User warnings from the geometry model. Which is an object containing a string property my-length-warning
  2. __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.

Set a length between 0 and 2000mm
Please select a width