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 in
  • optionId -> This the the ID of the option
  • replacementId -> This the the exact replacement you want to activate. NOTE - this is NOT the replacementDetailsId. Find replacementId 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.