Include Your React Widgets in Any Web Page
Tags: react, javascript, typescript
Published: 2020-05-11 13:37:02

Visit my Blog for the original post: [Include Your React Widgets in Any Web Page](https://yizhiyue.me/2020/05/11/include-your-react-widgets-in-any-web-page/)

Hello everyone! It's been a long time since my last post because I was so busy with my study in the last semester and, of course, damn COVID-19! But anyway, I finally graduate from the university, so it's time to make a post! Woohoo!!

To combat COVID-19, I participated a government open source project, which requires me to build a react widget library and integrate to any web pages which can be written in any web frameworks, such as Angular, Vue or even Svelte.

Initially, I thought I may need web component to solve the problem, but eventually, I found a native javascript way to solve it, thanks to [Dhrubajit](https://www.linkedin.com/in/dhrubajitchowdhury/) for inspiring me!

**The code is [HERE](https://github.com/ZhiyueYi/demo-react-web-widget) and the Demo is [HERE](http://yizhiyue.me/demo-react-web-widget/)**

# Main Idea

To make the widget universal for all kinds of web applications, we can utilise `<script>` tag, which can load a bundled JavaScript file including the React run time and the widget itself. Then, in the target web page, we can create a `<div>` tag with a specific ID. Based on the ID, we can render the widget inside of the `<div>` tag. In this way, we can load our React widget in any web page, because they are all basically JavaScript!

The target of this demo is to allow the following HTML codes

```html
<div id="simple-calendar" is-click-disabled min-year="2020"></div>
<script src="./simple-calendar.js"></script>
```

To render an equivalent react component as

```jsx
<SimpleCalendar id="simple-calendar" isClickDisabled={true} minYear={2020}>
```

# Setup

For the setup, you can just follow what I did in the demo project. The widget source codes are included in the `src` folder and the demo web page is in the `docs` folder.

The idea is that, we need to use Webpack to bundle whatever in the `src` folder, and then produce the bundled JavaScript file named `simple-calendar.js` to the `docs` folder. Then, the `docs/index.html` page can load the bundled JavaScript file by `<script src="./simple-calendar.js"></script>`. In this way, the React widget `SimpleCalendar` can be rendered in the `docs/index.html` page.

What's most interesting is that, `docs/index.html` is just a plain static web page, and it can still pass initial configurations to the `<div>` tag to render the react `SimpleCalendar` component.

# Webpack Configuration

It's fairly easy to create a simple WebPack configuration for this Demo. Basically we just want to bundle the whole `src` folder with the entry file `index.ts`, and then output the bundled file to `docs` folder with a name as `simple-calender.js`.

I recommend you to read [Demystifying Webpack](https://github.com/DhrubajitPC/webpack-react-todo/blob/master/Demystifying%20Webpack.md) written by my good friend [Dhrubajit](https://www.linkedin.com/in/dhrubajitchowdhury/) because his tutorial on Webpack configuration is **<span style="background: lightgrey; padding: 2px 5px;"><span style="color: red">A</span><span style="color: orange">W</span><span style="color: yellow">E</span><span style="color: green">S</span><span style="color: cyan">O</span><span style="color: blue">M</span><span style="color: purple">E</span></span>**!

```typescript
const path = require("path");

const config = {
  entry: path.join(__dirname, "./src/index.ts"),
  output: {
    path: path.resolve(__dirname, "./docs"),
    filename: "simple-calendar.js",
  },
  devtool: "source-map",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".css", ".txt"],
  },
  module: {
    rules: [
      {
        test: /\.ts(x?)$/,
        exclude: /node_modules/,
        include: [path.resolve("src")],
        loader: "ts-loader",
        options: {
          transpileOnly: false,
          compilerOptions: {
            module: "es2015",
          },
        },
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

module.exports = config;
```

# HTML Tag Wrapper

Here comes the core function to achieve including react widgets in any web page, which is to find the element from the web page with a pre-defined element ID.

And then we need to get all the HTML tag attributes from the element, parse them into a key-value pair object, dump the object into the React Component (which is our SimpleCalendar component in `src/simple-calendar.tsx` file) and lastly, use ReactDOM to render the component.

```typescript
function HtmlTagWrapper(Component: (props?: any) => JSX.Element) {
  const el = document.getElementById("simple-calendar");
  const attrs = el.attributes;

  const props = attrToObj(attrs);
  console.log(props);
  ReactDOM.render(<Component {...props} />, el);
}
```

# Attributes to Object

It's actually a bit tricky to transform the attributes to key-value pair objects, because attributes are of the `NamedNodeMap` type. According to [NamedNodeMap - Mozilla](https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap), a `NamedNodeMap` object can be access by index as in an array, or by key name as in an object.

```typescript
function attrToObj(attrs: NamedNodeMap) {
  const attrsObj: { [key: string]: unknown } = {};
  const length = attrs.length;
  for (let i = 0; i < length; i++) {
    const { name, value } = attrs[i];
    attrsObj[parseKey(name)] = parseValue(value);
  }
  return attrsObj;
}
```

In the above code snippet, I can simply grab the `name` and `value` from `attrs[i]`.

And here comes another tricky part for `name` and `value`. I have to parse them so that the constructed `attrsObj` can have correct keys and values.

# Parse Keys and Values

Let's say you have a simple HTML div tag as `<div id="simple-calendar" is-click-disabled min-year="2020">`

You intend to have an attrsObj to be constructed as

```json
{
  "id": "simple-calendar",
  "isClickDisabled": true,
  "minYear": 2020
}
```

However, from `const { name, value } = attrs[i];`, the values you get are all strings, the keys you get are all small letters connected with `-`

Therefore, you need to parse them, so that the values can be string, number, boolean respectively and the keys can be in camel cases without delimiters.

```typescript
function parseValue(value: any) {
  if (value === "" || value === "true") {
    return true;
  }

  if (value === "false") {
    return false;
  }

  if (Number(value).toString() === value) {
    return Number(value);
  }

  return value;
}

function parseKey(key: string) {
  const parts = key.split("-");
  const newParts = [parts[0]];
  for (let i = 1; i < parts.length; i++) {
    const firstLetter = parts[i].slice(0, 1);
    const restOfLetters = parts[i].slice(1);
    const newPart = firstLetter.toUpperCase() + restOfLetters;
    newParts.push(newPart);
  }
  return newParts.join("");
}
```

# Put Them Together

By implementing all of parts I mentioned above, finally you can just write an HTML div tag as

```html
<div id="simple-calendar" is-click-disabled min-year="2020"></div>
<script src="./simple-calendar.js"></script>
```

To render an equivalent react component as

```jsx
<SimpleCalendar id="simple-calendar" isClickDisabled={true} minYear={2020}>
```

And most importantly, you can put your `<div>` and `<script>` in any kind of web page because they are simply standard JavaScript and HTML!