Implementing External Widgets in Vue.js

Embedding an independent component in a third party website.

As a developer, we might come across a situation where we want to inject some component or an app (or a part of an app) in some external application. Such components are called widgets. Widgets are basically components which can be embedded in a third party website or your own website too. They commonly provide users with the third party app access to resources from another website. Google Adsense is an example of a widget. Sometimes we see a little "Chat for Help" button on a webpage, which is also a widget.

In this article, we will try to make a widget using Vanilla JS, that would be embedded in an external app made using Vue. We can also use React for that.

So, let's get started.

What are we building?

I know I said we are going to create a widget, but what is that widget going to be exactly? Before starting something, we should know what the problem statement is. We want to have an independent component in an external website that would allow the user to interact with that component and give the control to our main app, and all this has to be done without much manipulation of the external app's existing code.

To solve this, we will create a widget (this term is going to be used a LOT throughout). Next, we need a scenario. We will have an external application which will be called Geeky Glasses, which is an app for a company that manufactures cool glasses 😎. Our main app would be a sample e-commerce company called BLAH, which is going to take some details from a user to facilitate pre-bookings for those glasses. Our widget would be a small form created by BLAH and that would be injected in Geeky Glasses's home page.

When we are done we will see something like this . For the full code, visit github.com/shreygeekyants/Vue-Widget

Get your tools ready

We will proceed our development in the following order:

  • External Webpage (HTML/CSS).
  • Main App (Vue/React).
  • Widget (Javascript).

External Webpage

For simplicity, we will create a simple web page that would act as an external webpage for Geeky Glasses. We will use plain HTML and CSS to make this page. Let's add some content to the body of the page.

<body>
    <div class="main-div">
      <div class="left-panel">
        <div class="main-text">
          <span style="font-size: 90px; color: #099ba3;">Geeky</span>
          <span style="font-size: 40px;">GLASSES</span>
          <div class="subtext">Coming soon...</div>
        </div>
        <img src="./product.png" width="100%" />
      </div>
      <div class="right-panel">
        <div id="widget" class="widget">
            OUR WIDGET WILL BE RENDERED HERE  
        </div>
      </div>
    </div>
</body>

Our external webpage would look like this and the widget will be rendered in the blank space.

Screenshot 2021-01-04 at 7.21.01 PM.png

We will later add a script in this webpage which will fetch the widget that we'll create.

Main App

We will setup a Vue (or React) project, which will here be an e-commerce website for BLAH and create a multi-step form that would allow the user to enter their personal and address details to proceed the pre-bookings of Geeky Glasses.

To create a vue project, run:

     vue create vue-widget

Choose your desired features either default or manual and set up the app. We can now create a form with basic validations and a success screen. The personal details of the form will be filled up with the details that the user will enter in the external app using our widget.

You can check the entire code of our main app on Github. It would now look something like this:

Screenshot 2021-01-04 at 7.37.52 PM.png

Okay, so now we have our external and main app ready. The fun part comes now,where we connect Geeky Glasses and BLAH.

Widget

Before we start the implementation, let's understand how the widget would work. As I stated before, we will include a script in the external webpage that would render the widget. The script is appended in the head tag of the html file. The script actually resides in our main app as a static asset which can be accessed using the absolute URL of the app. Let's add the script in our external webpage.

<script>
  var js = document.createElement("script");
  js.async = true;
  // Path to the script that loads the widget
  js.src = "https://vuewidget.herokuapp.com/widget-loader.js"; // This is where the app is currently hosted
  document.getElementsByTagName("head")[0].appendChild(js);
</script>

In the above code snippet, we can see that widget-loader.js is accessed as a static asset. This is because we have kept it in the public folder and it does not go through the webpack. We can reference static assets through absolute paths.

Coming to widget-loader.js, it is a self invoking JS function that loads a CSS file (widget.css) and a JS file (widget.js) and appends it to head and script tag of the external webpage respectively. The JS file will specify the actual HTML code for the widget and the CSS file will be styling it. This is what widget-loader.js looks like:

// Self invoking function
(function(w, d, link, script, p) {
  window.onload = function() {
    // Load css
    var css = "https://vuewidget.herokuapp.com/css/widget.css";
    // Load js
    var js = "https://vuewidget.herokuapp.com/js/widget.js";

    link = d.createElement("link");
    link.rel = "stylesheet";
    link.href = css;
    // Appending stylesheet in the head tag
    d.getElementsByTagName("head")[0].appendChild(link);

    script = d.createElement("script");
    script.async = 1;
    script.src = js;
    // Adding the script in the script tag
    p = d.getElementsByTagName("script")[0];
    p.parentNode.insertBefore(script, p);
  };
})(window, document);

Note that widget.css and widget.js are also added in the public folder and made available as static assets which can be referenced using absolute paths.

Widget.js is also a self invoking function. We will create an object constructor called Widget, which will have the following properties:

  • html: this contains the raw html code as a string.
  • options: this contains containerID and formName.

The containerID is the id of the div that will contain the widget.

Remember this part:

<div id="widget" class="widget">
  OUR WIDGET WILL BE RENDERED HERE  
</div>

Next, we call two functions initializeWidget and initializeEvents. InitializeEvent adds the html property in the innerHTML object of the containerID and InitializeEvents adds the submit and button click event. Widget.js looks like this:

(function() {
  this.Widget = function() {
    // HTML source code for the widget
    this.html =
      `<div class='container'>` +
      `<div class='discount-text'>Get 10% Cashback on early bookings</div>` +
      `<div class='logo'><span>b</span><span>l</span><span>a</span><span>h</span>.com</div>` +
      `<form id='form' name="widget-form" action="" target="_blank" style="margin-top:5%">` +
      `   <div class="widget-row" >` +
      `       <div class="widget-cell">` +
      `           <label class="widget-label" id="widget-firstname-label" for="widget-firstname">Firstname</label>` +
      `           <input type="text" id="widget-firstname" class="widget-input" name="firstname" />` +
      `       </div>` +
      `       <div class="widget-cell">` +
      `           <label class="widget-label" id="widget-lastname-label" for="widget-lastname">Lastname</label>` +
      `           <input type="text" id="widget-lastname" class="widget-input" name="lastname" />` +
      `       </div>` +
      `   </div>` +
      `   <div class="widget-row">` +
      `       <div class="widget-cell">` +
      `           <label class="widget-label" id="widget-email-label" for="widget-email">Email ID</label>` +
      `           <input type="text" id="widget-email" class="widget-input" name="email" />` +
      `       </div>` +
      `       <div class="widget-cell">` +
      `           <label class="widget-label" id="widget-phone-label" for="widget-phone">Phone</label>` +
      `             <input type="number" id="widget-phone" class="widget-input" name="phone" />` +
      `       </div>` +
      `   </div>` +
      `   <button id='submit-button'>BOOK NOW</button>` +
      `</form>` +
      `</div>`;
    var defaults = {
      containerId: "widget",
      formName: "widget-form",
    };
    this.options = defaults;

    initializeWidget(this);
    initialiseEvents(this);
  };
  function initializeWidget(self) {
    var container = document.getElementById(self.options.containerId);
    if (container) {
      // Appending the widget html code to the block which has the id "widget" in the demo page
      container.innerHTML = self.html;
    }
  }
  function initialiseEvents(self) {
    // Adding event listener to the "Book Now" button
    var button = document.getElementById("submit-button");
    if (button) {
      button.addEventListener("click", submitClicked.bind(self));
    }
    var form = document.getElementById("form");
    if (form) {
      form.addEventListener("submit", submitClicked.bind(self));
    }
  }

  function validateFields(field, value) {
    var regex = /[a-z]{2,}/i;

    // Validate first and last name
    if (field === "firstname" || field === "lastname") {
      return regex.test(value);
    }
    // Validate email address
    if (field === "email") {
      regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

      return regex.test(value);
    }
    // Validate phone number
    if (field === "phone") {
      regex = /^[0-9]{10}$/;
      return regex.test(value.toString());
    }
  }

  function submitClicked(event) {
    if (event) {
      event.preventDefault();
    }
    var fields = ["firstname", "lastname", "email", "phone"],
      form = document.forms[this.options.formName],
      field,
      label;
    for (var i = 0; i < fields.length; i++) {
      field = form[fields[i]];
      field.className = field.className.replace(" widget-error", "");
      label = document.getElementById("widget-" + fields[i] + "-label")
        .innerText;
      if (!field.value) {
        field.className += " widget-error";
        alert("Please enter your " + label);
        field.focus();
        return false;
      }
      if (!validateFields(fields[i], field.value)) {
        field.className += " blinker-error";
        alert("Please enter a valid " + label);
        field.focus();
        return;
      }
    }
    // Opening the vue app with all the details
    window.open(
      `https://vuewidget.herokuapp.com/?firstname=${form["firstname"].value}&lastname=${form["lastname"].value}&email=${form["email"].value}&phone=${form["phone"].value}`,
    );
  }
})();

new Widget({});

Deploying the app

We can now deploy our app. I have used Heroku for the "blink-of-an-eye" deployment. Once deployed, we can see that external webpage now has the widget rendered in the blank space. We can enter our details and click on the submit button. It will redirect us to the main app with some fields pre-filled.

Widget.gif

Packing up

So, this is how you can implement a widget. For React, the process is entirely the same. We can create a widget of any sort using this method. Please feel free to drop a ⭐️ on github.com/shreygeekyants/Vue-Widget if it worked out for you.

Thank you for reading. Stay awesome!

No Comments Yet