Outerbase Plugin | Creating an HTML Preview Plugin

Makena KongMakena Kong
5 min read

What is it?

This plugin lets you preview your HTML content stored in a table column. This is perfect if you are storing data for blog posts or lengthy user bios.

TL;DR - just copy this code and get started!

Note - this plugin only works with the Outerbase Light Theme.

How do I use it?

Watch the demo video!

  1. Add the plugin to your column.

  2. Click the “Preview” button in any of the column cells.

  3. A modal will pop up displaying your rendered HTML!

What’s required?

You need a database (obviously), a table (of course) and a column with a text or varchar data type - or any data type that holds a string. The actual values would be a string of text or a string of decently encoded HTML.

If you are using a SQLite database - you can create and populate your database with sample Blog Post data by running these SQL queries.

What if the value is null?

It'll say “Nothing to preview.”

What if the value has bad HTML?

It'll say “Failed to load preview” with a descriptive error message and your raw string. Hopefully, you will never see this state because you are properly sanitizing and encoding your HTML before storing it in your database.

Explain the code to me!

Privileges

This is a column plugin so all we need access to is the cellValue and the configuration.

var privileges = ["cellValue", "configuration"];

Configuration

This code creates and initializes objects for the plugin’s configuration. We don't need to add any more to this.

class OuterbasePluginConfig_$PLUGIN_ID {
  constructor() {}
}

Modifying the Cell

This code adds a value input and a “Preview” button to the cell. The user can also still click the cell to paste in or edit the value. Clicking the “Preview” button should open a modal with the rendered HTML or an error message.

var templateCell_$PLUGIN_ID = document.createElement("template");
templateCell_$PLUGIN_ID.innerHTML = `
<style>
  #container { 
    display: flex;
    align-items: center;
    gap: 8px;
    justify-content: space-between;
    height: 100%;
    width: calc(100% - 16px);
    padding: 0 8px;
  }

  input {
    height: 100%;
    flex: 1;
    background-color: transparent;
    border: 0;
    min-width: 0;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    font-family: input-mono,monospace;
    margin: 0;
    padding: 10px 5px;
  }

  input:focus {
    outline: none;
  }

  button {
    border: 1px solid black;
    border-radius: 2px;
    background-color: white;
    cursor: pointer;
  }
</style>

<div id="container">
  <input id="input" />
  <button id="open-preview">Preview</button>
</div>
`;

class OuterbasePluginConfig_$PLUGIN_ID {
  constructor() {}
}

class OuterbasePluginCell_$PLUGIN_ID extends HTMLElement {
  static get observedAttributes() {
    return privileges;
  }

  config = new OuterbasePluginConfig_$PLUGIN_ID({});

  constructor() {
    super();

    this.shadow = this.attachShadow({ mode: "open" });
    this.shadow.appendChild(templateCell_$PLUGIN_ID.content.cloneNode(true));

    this.config = new OuterbasePluginConfig_$PLUGIN_ID(
      JSON.parse(this.getAttribute("configuration"))
    );
  }

  connectedCallback() {
    this.shadow.querySelector("input").value = this.getAttribute("cellValue");

    this.shadow
      .querySelector("#open-preview")
      .addEventListener("click", () => this.showPreview());
  }

  showPreview() {
    const event = new CustomEvent("custom-change", {
      detail: { action: "onedit", value: true },
      bubbles: true,
      composed: true,
    });

    this.dispatchEvent(event);
  }
}

Cell Editor

This code renders a modal with the rendered HTML of the cell value.

  1. Check if the cell has a value - else render a header but no content.

  2. Check if the cell has valid HTML - else render a header, a detailed error message, and the cell value as a string.

  3. If the value is valid HTML, render a header and the cell value as rendered HTML.

var templateEditor_$PLUGIN_ID = document.createElement("template");
templateEditor_$PLUGIN_ID.innerHTML = `
  <style>
    #container {
      position: absolute;
      top: -50px;
      left: calc(-25vw + 25%);
      width: 50vw;
      height: auto;
      min-width: 600px;
      max-width: 1000px;
      background: white;
      padding: 20px;
    }

    #close-preview {
      border: none;
      background: none;
      color: black;
      float: right;
      font-family: inter;
      font-size: 24px;
      font-weight: 600;
      cursor: pointer;
    }

    #close-preview:hover {
      opacity: 0.8;
    }

    #header {
      font-family: inter;
      font-weight: 600;
      font-size: 24px;
    }

    #error {
      display: none;
      margin: 10px 0;
      font-family: inter;
      font-weight: 400;
      font-size: 16px;
      color: #B22222;

      -moz-user-select: text;
      -khtml-user-select: text;
      -webkit-user-select: text;
      -ms-user-select: text;
      user-select: text;
    }

    #content {
      height: 400px;
      overflow: scroll;

      -moz-user-select: text;
      -khtml-user-select: text;
      -webkit-user-select: text;
      -ms-user-select: text;
      user-select: text;
    }
  </style>

  <div id="container">
    <button id="close-preview" title="close">x</button>
    <h1 id="header"></h1>
    <hr/>
    <div id="error"></div>
    <div id="content"></div>
  </div>
`;


class OuterbasePluginEditor_$PLUGIN_ID extends HTMLElement {
  static get observedAttributes() {
    return privileges;
  }

  static NO_VALUE_HEADER = "Nothing to preview";
  static BAD_VALUE_HEADER = "Failed to load preview";
  static PREVIEW_HEADER = "Preview";

  config = new OuterbasePluginConfig_$PLUGIN_ID({});

  constructor() {
    super();

    this.shadow = this.attachShadow({ mode: "open" });
    this.shadow.appendChild(templateEditor_$PLUGIN_ID.content.cloneNode(true));

    this.config = new OuterbasePluginConfig_$PLUGIN_ID(
      JSON.parse(this.getAttribute("configuration"))
    );
  }

  connectedCallback() {
    const value = this.getAttribute("cellValue");

    this.shadow
      .querySelector("#close-preview")
      .addEventListener("click", () => this.closePreview());

    if (this.isEmpty(value)) {
      this.shadow.querySelector("#header").innerHTML =
        OuterbasePluginEditor_$PLUGIN_ID.NO_VALUE_HEADER;
      return;
    }

    var error = this.isInvalidHTML(value);
    if (error) {
      this.shadow.querySelector("#header").innerHTML =
        OuterbasePluginEditor_$PLUGIN_ID.BAD_VALUE_HEADER;
      this.shadow.querySelector("#error").style.display = "block";
      this.shadow.querySelector("#error").innerHTML = error.innerHTML;
      this.shadow.querySelector("#content").innerText = value;
      return;
    }

    this.shadow.querySelector("#header").innerHTML =
      OuterbasePluginEditor_$PLUGIN_ID.PREVIEW_HEADER;
    this.shadow.querySelector("#content").innerHTML = value;
  }

  closePreview() {
    const event = new CustomEvent("custom-change", {
      detail: { action: "onstopedit", value: true },
      bubbles: true,
      composed: true,
    });

    this.dispatchEvent(event);
  }

  isEmpty(data) {
    return !data || data.length == 0;
  }

  isInvalidHTML(data) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(data, "application/xml");
    return doc.querySelector("parsererror");
  }
}

Define our Plugin's Web Components

This code makes sure our Web Components are available when you attach the plugin to your column.

window.customElements.define(
  "outerbase-plugin-cell-$PLUGIN_ID",
  OuterbasePluginCell_$PLUGIN_ID
);

window.customElements.define(
  "outerbase-plugin-editor-$PLUGIN_ID",
  OuterbasePluginEditor_$PLUGIN_ID
);

Results

Your cell should look something like this:

Your table/modal should look something like this:

Limitations?

Light/Dark Theme

The tailwind classes weren't working properly so the styles are hardcoded to work in a Light Theme.

Custom Styling

In the future, it would be cool to let the user configure their plugin with an external stylesheet so their HTML previews are styled as they would be in their application. I didn’t see much documentation about how to set this up - so we’re going with the default styles.

Outstanding Issues?

I could not figure out how to center the modal.

The editor plugin seems to be within a relatively positioned element - so the absolute positioned modal has to work around it. There are some top/left calculations - but this is an issue if your database has more than 10 rows or more - unless you have a long desktop screen.

Thanks for reading!

Let me know if you have any questions or suggestions!

12
Subscribe to my newsletter

Read articles from Makena Kong directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Makena Kong
Makena Kong