Build a "Pluggable" Widget for your Web App
Tags: react, javascript, webdev, tutorial
Published: 2020-09-12 13:56:45

First of all, what is a widget? A widget is any applicaton 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](https://github.com/SoorajSNBlaze333/react-webpack-ts-starter) template code. To know how this template was created, be sure to check [this](https://www.skcript.com/svr/using-webpack-with-react-typescript/) 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**

```js
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**](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**

```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,

```sh
npm run dev
```

Now if you go to [http://localhost:9000/Widget.js](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**

```js
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**

```js
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](https://github.com/SoorajSNBlaze333/react-widget-ts-webpack-starter). 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.

```html
<!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,

```sh
npm run start
```

and open [http://localhost:5000](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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8xuucvhlorki9rktoffx.gif)