Include Your React Widgets in Any Web Page
May 11, 2020
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 for inspiring me!
The code is HERE and the Demo is HERE
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
<div id="simple-calendar" is-click-disabled min-year="2020"></div>
<script src="./simple-calendar.js"></script>
To render an equivalent react component as
<SimpleCalendar id="simple-calendar" isClickDisabled={true} minYear={2090}>
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 written by my good friend Dhrubajit because his tutorial on Webpack configuration is AWESOME!
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.
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, a NamedNodeMap
object can be access by index as in an array, or by key name as in an object.
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
{
"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.
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
<div id="simple-calendar" is-click-disabled min-year="2020"></div>
<script src="./simple-calendar.js"></script>
To render an equivalent react component as
<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!
Featured image is credited to Format from Pexels