Build a “Pluggable” Widget for your Webapp

Ever wanted to create an interactive widget that could just plugin to any of your webapps and work seamlessly? No? Well I’ll still write about it. In this article, I will be taking you through a step by step walkthrough on how to create a small popup widget using React and Typescript.

Date

Written By

Build a “Pluggable” Widget for your Webapp

First of all, what is a widget? A widget is any application that is a miniature version of the original application. You can make a widget out of any application you want.

You might have seen them in mobile and computers as small floating windows. For example, you have an application, a small floating widget of your favorite music application. This widget will not only make space for other widgets, but also give you access to a minimal version of the full blown application.

Widgets basically reduce the effort in interacting with the application. One of the use cases could be a “view only widget”, where all the “readonly” data is displayed on the widget and the modify or write actions are performed on the application. This way, you can provide your user a slimmed down version which is easier to use.

Let us create a simple widget app with 3 pages with Create, List and Update operations. We will be using the following

  • React as the UI Framework.
  • Typescript as the coding language.
  • Bootstrap for design.
  • Webpack to configure and build the app.
  • Local Storage of the browser for data storage.

First let us create a React app. For this tutorial we will be using this template code. To know how this template was created, be sure to check this out.

We will just clone this template and modify the code for the widget. Since our widget will be running inside an iframe, we will not be able to make use of react-routers. So in our case we will use conditional rendering using switch cases to render the components based on a page variable.

After cloning the template and installing the packages, let us start creating an entry point for our widget to initialize. Let us create a file called widget.ts under the src folder. This file will contain all the configuration for setting up and rendering the iframe.

So, it is just basically 2 things combined. You have your normal react app that will be run by the widget.ts inside an iframe and be pluggable anywhere. Since we know that we cannot communicate props directly between the window and an iframe, we need to use the postMessage function to talk between the iframe and window and exchange props or values.

All of these might sound confusing in the start, but will become easier once we go step by step.

Now we can start adding code to the widget.ts file. We will first create our widget object that will be used to configure and initialize from the webpage that is going to use the widget. Let’s do something simple.

widget.ts

const defaultStyles: any = {
 'border': 'none',
 'z-index': 2147483647,
 'height': '650px',
 'width': '350px',
 'display': 'block !important',
 'visibility': 'visible',
 'background': 'none transparent',
 'opacity': 1,
 'pointer-events': 'auto',
 'touch-action': 'auto',
 'position': 'fixed',
 'right': '20px',
 'bottom': '20px',
}

interface IConfig {
 readonly email: string;
}

interface IWidget {
 config: IConfig | null;
 iframe: HTMLIFrameElement | null;
 init: (config: IConfig) => void;
 setupListeners: () => void;
 createIframe: () => void;
 handleMessage: (event: MessageEvent) => void;
}

const Widget: IWidget = {
 iframe: null,
 config: null,
 init: function(config: IConfig) {
   this.config = config;
   this.createIframe()
 },
 createIframe: function() {
   this.iframe = document.createElement('iframe');
   let styles = '';
   for (let key in defaultStyles) { styles += key + ': ' + defaultStyles[key] + ';' }
   this.iframe.setAttribute('style', styles)
   this.iframe.src = 'http://localhost:9000';
   this.iframe.referrerPolicy = 'origin';
   document.body.appendChild(this.iframe);
   this.setupListeners();
 },
 setupListeners: function() {
   window.addEventListener('message', this.handleMessage.bind(this));
 },
 handleMessage: function(e) {
   e.preventDefault();
   if (!e.data || (typeof e.data !== 'string')) return;
   let data = JSON.parse(e.data);
   switch (data.action) {
     case 'init': {
       if (this.iframe) {
         this.iframe.contentWindow.postMessage(JSON.stringify(this.config), '*');
       }
       break;
     }
     default:
       break;
   }
 }
};

export default Widget;

The init function will be used in the script tag and the rest is used to build and set up the widget. The handleMessage function will be used to communicate with the React application to pass data across both the iframe and the parent. So here we will just get the configuration that is passed at the script tag in the webpage that uses the widget and pass it in the config variable to the React app. Here we see that the iframe src is http://localhost:9000. This will be our React app server. Now in order to load the widget onto a page, we need to first configure our webpack file in a different way.

webpack.config.js

const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');

const isProd = process.env.NODE_ENV === 'production';

const config = {
 mode: isProd ? 'production' : 'development',
 entry: {
   app: [
     'webpack-dev-server/client?http://0.0.0.0:9000/',
     'webpack/hot/only-dev-server',
     './src/index.tsx'
   ],
   Widget: ['./src/widget.ts']
 },
 output: {
   filename: '[name].js',
   path: resolve(__dirname, 'dist'),
   library: '[name]',
   libraryTarget: 'umd',
   libraryExport: 'default'
 },
 resolve: {
   extensions: ['.js', '.jsx', '.ts', '.tsx'],
 },
 module: {
   rules: [
     {
       test: /\.tsx?$/,
       use: 'babel-loader',
       exclude: /node_modules/,
     },
     {
       test: /\.css?$/,
       use: [
         'style-loader',
         { loader: 'css-loader', options: { importLoaders: 1 } },
         'postcss-loader'
       ]
     },
   ],
 },
 plugins: [
   new HtmlWebpackPlugin({
     template: './src/index.html',
     hash: true,
     filename: 'index.html',
     inject: 'body',
     excludeChunks: ['widget']
   }),
 ],
};

if (isProd) {
 config.optimization = {
   minimizer: [new TerserWebpackPlugin(),],
 };
} else {
 config.devServer = {
   port: 9000,
   open: true,
   hot: true,
   compress: true,
   stats: 'errors-only',
   overlay: true,
 };
}

module.exports = config;

We will change the entry of our app to load the react app as app and the widget.ts as Widget. And in our HTMLPlugin we will tell webpack to exclude the widget from the chunks.

We are now ready to set up our server. We will run,

npm run dev

Now if you go to http://localhost:9000/Widget.js, we will see our widget.ts compiled code there. If it does not show any error, then we are good to go. We are ready to move to the React App set up now.

Since we need to only load the widget if we receive the config value, we will need to listen for the postMessage.

index.tsx

import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { IConfig } from './config/interfaces';
import { Context } from './context/context';
import './stylesheets/index.css';

window.addEventListener('DOMContentLoaded', (event) => {
 window.parent.postMessage(JSON.stringify({ action: 'init' }), '*');
 window.removeEventListener('DOMContentLoaded', () => null);
});

window.addEventListener('message', (event) => {
 event.preventDefault();
 if (!event.data || (typeof event.data !== 'string')) return;
 const config: IConfig = JSON.parse(event.data);
 return render(
   <Context.Provider value={JSON.stringify(config)}>
     <App />
   </Context.Provider>,
   document.body
 );
});

Once the DOM is loaded, we will send a message to the iframe with the action init to tell the iframe that the react app has been loaded onto the DOM. The iframe checks the action in the handleMessage function used in widget.ts and sends back a message with the config data. The React app will listen to this message and call the render method if the config exists. This will ensure that the widget always loads only after the config is present.

Now that our React app is loaded, we will create our conditional routing in the App.tsx.

App.tsx

import React, { useContext, useState } from 'react';
import { IConfig } from './config/interfaces';
import { Context } from './context/context';
import Active from './components/Active';
import Completed from './components/Completed';
import NewTask from './components/NewTask';

const App: React.FC = (props) => {
 const config: IConfig = JSON.parse(useContext(Context));
 const [page, setPage] = useState<Number>(1);
  const renderHeader = () => {
   return (<h3 className="bg-dark p-3 m-0 text-white">Todo-List</h3>);
 }

 const renderLinks = () => {
   return (<div className="nav row m-0 bg-light">
     <a className="nav-link col-4 text-center" href="#" onClick={() => setPage(1)}>Active</a>
     <a className="nav-link col-4 text-center" href="#" onClick={() => setPage(2)}>New</a>
     <a className="nav-link col-4 text-center" href="#" onClick={() => setPage(3)}>Completed</a>
   </div>)
 }

 const renderComponent = () => {
   switch(page) {
     case 1: return <Active config={config}/>
     case 2: return <NewTask setPage={setPage}/>
     case 3: return <Completed config={config}/>
     default: return <Active config={config}/>
   }
 }

 return (<div className="h-100 w-100 border rounded">
   {renderHeader()}
   {renderLinks()}
   {renderComponent()}
 </div>);
}

export default App;

Here I have just made a simple Todo List App. For the full code please refer here. The current page is a state variable and is changed whenever the link is clicked. And the components for the respective pages are loaded based on a switch statement. After setting up all the pages, we will now call the widget method in our html page.

For the test I have created a file called index.html in the dist folder with the following code.

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Webpack with React TS</title>
</head>
<body>
 <script src="http://localhost:9000/Widget.js"></script>
 <script>
   const config = { email: 'sooraj@skcript.com' };
   Widget.init(config);
 </script>
</body>
</html>

And we are done setting up. Just run this file using the,

npm run start

and open http://localhost:5000. Now we have the entire React app that you built rendered in an iframe and can be plugged into any site with the script above.

Here is a demo of the React App that was made as a widget.

Up next

Checklist to Improve your UI Design

Subscribe to Sudo vs Root

Our newsletter rolls out every month. No fluff. Pure content.

Skcript https://www.skcript.com/svr/build-a-pluggable-widget-for-your-webapp/ https://www.skcript.com/svrmedia/heroes/build-a-pluggable-widget-for-your-webapp.png

We collaborate with ambitious brands and people.

Get free consulting →