Basic Webpack setup for creating a JS widget w/ a loading script.

Package.json

Include Webpack & Webpack CLI

{
  "name": "widget",
  "version": "1.0.0",
  "description": "Widget widget",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack && cp dist/*.js ../public/js/",
    "watch": "webpack --watch"
  },
  "author": "",
  "license": "None",
  "devDependencies": {
    "webpack": "^4.27.1",
    "webpack-cli": "^3.1.2"
  }
}

Webpack

Basic config for Webpack 4. We need to output two files, one for the loader, which shouldn’t change much and one for the actual widget.

const debug = process.env.NODE_ENV !== 'production';
const Webpack = require('webpack');
const path = require('path');

const commonPlugins = [
  new Webpack.DefinePlugin({
    WIDGET_HOST: debug ?
      JSON.stringify("http://widget.test") :
      JSON.stringify("https:/widget.io")
  })
];

module.exports = {
  entry: {
    widget: './src/widget.js',
    viewer: './src/viewer.js',
  },
  devtool: debug ? 'inline-sourcemap' : false,
  mode: process.env.NODE_ENV || 'development',
  plugins: debug ? commonPlugins : [
    ...commonPlugins,
    // Add production plugins here!
  ],
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env']
          }
        }
      }
    ]
  }
};

Loader

The loaders main job is to load the actual widget script. It’s other job is to pass config info to the widget. This can be done with data-X attributes or a JS object.

'use strict';

function load_widget() {
  if (document.currentScript) {
    var script = document.currentScript;
  } else {
    const scripts = document.getElementsByTagName('script');
    const index = scripts.length - 1;
    var script = scripts[index];
  }

  if (!script.attributes["data-id"]) {
    console && console.log("Please put a data-id");
    return;
  }
  const id = script.attributes["data-id"].value;
  const pid = script.attributes["data-pid"].value;

  const js = document.createElement('script');
  js.src = WIDGET_HOST + '/js/viewer.js';
  js.onload = function() {
    widget.inject(id, pid);
  };
  script.parentNode.insertBefore(js, script);
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", load_widget);
} else {
  load_widget();
}

Widget

This is the functional part of the widget. This one loads an iframe, but it could manipulate the DOM directly.

'use strict';

window.widget = (function() {

  let rmr = {
    init: function(id, pid) {
      this.id = id;
      this.pid = pid;
      this.widget = document.getElementById('widget');
      if ( !this.widget) {
        console && console.log("widget div not found.");
        return false;
      }
      return true;
    },

    createButton: function() {
      var button = document.createElement('button');
      button.setAttribute('id', 'btn-widget');
      button.innerHTML = "Widget Has It";
      button.addEventListener('click', (e) => {
        e.preventDefault();
        this.createFrame();
      });
      this.widget.appendChild(button);
    },

    createFrame: function() {
      if ( document.getElementById("widget-view") ) {
        return false;
      }

      var iframe = document.createElement('iframe');
      iframe.setAttribute('src', WIDGET_HOST + '/sch/' + this.pid + '/locate');
      iframe.setAttribute('class', 'widget-iframe');
      iframe.setAttribute('data-id', this.id);
      iframe.setAttribute('data-pid', this.pid);
      iframe.setAttribute('width', '100%');
      iframe.setAttribute('frameborder', '0');
      iframe.setAttribute('scrolling', 'auto');
      iframe.style.border = 'none';
      iframe.style.width = '100%';
      iframe.style.height = '375px';
      iframe.style.position = 'relative';
      iframe.style.overflow = 'scroll';

      let view = document.createElement("div");
      view.setAttribute("id", "widget-view");

      let header = document.createElement("div");
      header.setAttribute("id", "widget-header");
      header.innerHTML = "Widget";

      let close = document.createElement("span");
      close.setAttribute("id", "widget-close");

      let closeA = document.createElement("a");
      closeA.setAttribute("href", "#");
      closeA.innerHTML = "CLOSE";
      closeA.addEventListener('click', (e) => {
        e.preventDefault();
        let view = document.getElementById("widget-view");
        document.getElementsByTagName("body")[0].removeChild(view);
        return false;
      });

      close.appendChild(closeA);
      header.appendChild(close);
      view.appendChild(header);
      view.appendChild(iframe);

      document.getElementsByTagName("body")[0].appendChild(view);
    },

    injectStyles: function() {
      let styles = `
        #widget-view {
          font-family: sans-serif;
          position: fixed;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          width: 640px;
          height: 400px;
          border: solid 5px black;
          z-index: 5000;
        }
        #widget-close a {
          color: white;
          text-decoration: none;
          font-size: 12px;
        }
        #widget-header {
          font-size: 18px;
          line-height: 22px;
          color: white;
          border-bottom: solid 5px black;
          height: 20px;
          padding: 0 10px;
          background-color: #353535;
        }
        #btn-widget {
          border: solid 2px #353535;
          background: #ddd;
          color: #353535;
          font-weight: bold;
          font-size: 14px;
          border-radius: 4px;
          padding: 2px 10px;
          cursor: pointer;
        }
        #widget-close {
          float: right;
          border-left: solid 5px black;
          height: 20px;
          padding: 0 10px;
        }`;
      let style = document.createElement("style");
      style.innerHTML = styles;
      document.getElementsByTagName("body")[0].appendChild(style);
    }
  };

  return {
    inject: function(id, pid) {
      if (rmr.init(id, pid)) {
        rmr.injectStyles();
        rmr.createButton();
      }
    }
  };

})();

References