Web widgets (Part 2): Widget him!
We build a widget and load it to the site
July 11, 2021In the previous post, I have introduced you to the world of web widgets. You have found out what a web widget is, for what you can use It, what functionalities it can provide, and why you need it. If you haven’t read the first part of this series, I recommend you to check it out.
In this part, I will show you how to make an example widget, and you will learn how to place it on any website.
Prerequisites
I assume you know the basics of Javascript, you know what functions and callbacks are.
You should also have at least basic knowledge of using one of the yarn or npm packet managers and be familiar with the package.json structure.
It’s also good to know what npx is.
Basic knowledge of React and create-react-app will also be helpful, as our widget will be built with it.
However, if you are not familiar with React it’s not a big issue, the examples are simple and it doesn’t require delving into the secrets of the framework.
The main point of this entry is to show you how to load a widget on a website, and we will use vanilla Javascript to make the loader.
Also: You will need some patience :)
Take a deep breath, look out the window and relax - you have a long moment to focus - we start coding!
How to make a widget?
Branch: stage-2-1
$ git clone git@github.com:chatscope/example-chat-widget.git
Don’t forget to run:
$ yarn install
or if you use npm:
$ npm install
Our widget will show birth statistics in Greenland, after all, everyone likes statistics, right?
Just kidding you :) Let’s make something cooler!
It will be a chat widget!
Notice!
We will not focus on communication here. You will not be able to send messages from the widget. We will make only a visual layer, to show how the widget loading works.
However, if you are interesting, how to make a real chat application, I will definitely write a post about it soon.
To make the widget, we are going to use my ready-made chat components library @chatscope/chat-ui-kit-react.
$ yarn add packet-name
If you prefer npm replace it with:
$ npm install packet-name
Creating a project and installing libraries
Let’s start by creating a React application. We’ll use create-react-app for this.
$ npx create-react-app example-chat-widget
After creating the application go to the app directory and install the libraries:
$ cd example-chat-widget$ yarn add @chatscope/chat-ui-kit-react @chatscope/chat-ui-kit-styles nanoid
Creating a widget - we code
Inside the src directory, create a new Widget.jsx file. In this file, we’ll define a React Widget component.
Import the styles required by @chatscope/chat-ui-kit-react library and components necessary to make the widget.
File src/Widget.jsxOpen link in new window :
1import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";2import { MainContainer, ChatContainer, ConversationHeader, MessageList, Message, MessageInput } from "@chatscope/chat-ui-kit-react";
Now create the Widget component in the same file.
4export const Widget = ({remoteName = "", messages = [], onSend}) => {5 6 return (7 <MainContainer>8 <ChatContainer>9 <ConversationHeader>10 <ConversationHeader.Content userName={remoteName} />11 </ConversationHeader>12 13 <MessageList>14 {messages.map( message =>15 <Message key={message._id} model={message} />16 )}17 18 </MessageList>19 20 <MessageInput placeholder="Type message here"21 attachButton={false}22 onSend={onSend}23 />24 </ChatContainer>25 </MainContainer>26 );27};
Our component accepts three props:
- messages - an array of chat messages to display
- onSend - callback that will be executed when the send button is clicked
- remoteName - the user name we chat with. It will be displayed in the header
The Widget component is dumb - it will show only what we provide to it from the outside.
Now let’s create a container responsible for widget business logic. It will react to every message sent, by pretending to be a remote user and reply with an echo to each message it received.
The src/WidgetContainer.jsxOpen link in new window file should contain:
1import { useState, useEffect } from "react";2import { Widget } from "./Widget";3import { nanoid } from "nanoid";4
5export const WidgetContainer = ({license = "", greeting = ""}) => {6
7 const [messages, setMessages] = useState([]);8
9 useEffect( () => {10 if ( greeting && messages.length === 0 ) {11 setMessages(messages.concat({12 _id: nanoid(),13 message: greeting,14 sender: "remote",15 direction: "incoming",16 }));17 }18 },[greeting, messages]);19
20 const handleSend = (message) => {21 const newMessages = [22 {23 _id: nanoid(),24 message,25 sender: "me",26 direction: "outgoing",27 },28 {29 _id: nanoid(),30 message: `ECHO: ${message}`,31 sender: "remote",32 direction: "incoming",33 }34 ];35 setMessages(messages.concat(newMessages));36 };37
38 return <Widget messages={messages} onSend={handleSend} />39
40};
A brief description of what’s going on here.
1import { nanoid } from "nanoid";
We use the nanoid library to generate a unique id for each message.
Why? Because React, when iterating over elements using the map method requires that each element has a unique (within a given iteration scope) key. It is used by React for performance optimization.
Such iteration is located in the <Widget /> component on line 14,
where we display a list of messages. Theoretically, the iteration counter, which is the second argument of the map function can be used as the key, but this is not recommended for performance reasons.
You can read more about this in the React documentation.
5export const WidgetContainer = ({license = "", greeting = ""}) => {
The license property will be passed from the loader. The widget will take advantage of it to personalize some functionality depending on its value.
We set this property to an empty string, and we will not use it in any way for now - we will come back to it later.
The greeting property is the welcome message that will appear first after the widget is loaded.
7const [messages, setMessages] = useState( []);
Above is an array for chat messages.
9useEffect( () => {10 if ( greeting && messages.length === 0 ) {11 setMessages(messages.concat({12 _id: nanoid(),13 message: greeting,14 sender: "remote",15 direction: "incoming",16 }));17 }18},[greeting, messages]);
If the greeting is not empty - it is added to the messages array in the first position.
20const handleSend = (message) => {
This function is called after pressing the send button or pressing the enter in the message input field. The function takes the text of the message as an argument.
We add each message sent to the array. Additionally, we create a new message preceded with the word “ECHO”, which pretends to be a reply from our interlocutor, and we also add it to the array.
After the changes, the App.js file should look like this:
File src/App.jsOpen link in new window :
1import './App.css';2import {WidgetContainer} from "./WidgetContainer";3
4function App() {5 return (6 <WidgetContainer />7 );8}9
10export default App;
Let’s add some styles. We remove the content of the src/index.css file and insert the new content:
File src/index.cssOpen link in new window :
1html, body {2 margin:0;3 padding: 0;4 width:100%;5 height:100%;6}
Similarly, in the src/App.cssOpen link in new window file:
1#root {2 position:relative;3 height:100%;4}
Our first widget is almost ready!
Run the project:
$ yarn run start
or if you use npm:
$ npm run start
Check if everything works. If there are no errors in the code, a chat window should appear after starting.
After typing a message and clicking the send button or pressing the “Enter” key, the typed message should be added to the message pane on the right, and just below it echoed as the message on the left.
Note, that the welcome message and the username you’re talking to are not shown yet.
Play with the chat for a while. If it is ok, let’s leave it for now and take care of its loading or… widget him :)
Loader - widget him!
Let’s move on to the key element - the loader.
First, I will explain to you how our loader will work.
The easiest way to avoid many problems such as: CSS, JS, and HTML code separation, localStorage overflow, and many others, is to put the widget in an iframe.
Google also uses iframes
You may have heard opinions, that the iframes are bad, that they should not be used, that they are obsolete, and so on. There are indeed reasons for this. Iframes can be bad for SEO, there are a few problems with styling, scrollers, and few other things….
However, It’s definitely not an obsolete or deprecated element, so don’t worry - you can and should use it but wisely. You will see for yourself that the iframe has plenty of the features we want.
Even Google uses iframes in Gmail and their other products.
- Most web-based email clients use iframes to display the body of an email (e.g. Roundcube)
- WYSIWYG editors also make frequent use of frames (e.g. TinyMCE)
- medium.com uses iframes to embed code examples - gists directly from GitHub - in the body of posts
- The vast majority of chat widgets used for customer support also load their widgets into iframes (eg. HubSpot, tawk.to). Do you think if the frame wasn’t suitable for this, they would be doing it?
How to do it?
Placing a widget application in an iframe is not difficult. It’s enough to programmatically create an iframe and enter the widget url in its src attribute.
We, however, want something more. Our loader should be as elegant as possible. We also want to be able to pass user parameters to the widget.
We will make the loader in such a way that the widget is loaded by adding the script tag to the website code. However, this tag will not contain a single line of javascript code inside.
Isn’t it going to be beautiful compared to the monstrosity you will find, for example in the Google Analytics embedding script 😉 ?
The data-* attributes added to the script tag will serve as additional configuration parameters for the loader and/or widget.
The code to load the widget into the webpage will look like this:
<script async src="loader.js" data-license="123" data-greeting="Hello!"></script>
This code can be placed in the head or anywhere in the body of the html document.
How can this be achieved? The concept is very simple!
Once loaded the loader.js script will find its own <script /> tag that loaded it. Then it will create an iframe, set some parameters for it, and load the widget into it.
Additionally, from the found script tag it will take attributes that parametrize the widget (data-license and data-greeting in this case) and will pass them to the loaded application.
This simple approach opens up great possibilities for controlling the widget and various types of interaction with it.
We won’t need any framework to make the loader. We will use pure javascript.
Preparing the project
Branch: stage-2-1
$ git clone git@github.com:chatscope/example-widget-loader.git
Don’t forget to run:
$ yarn install
or if you use npm:
$ npm install
The loader will be a separate project. So let’s create it.
$ mkdir example-widget-loader && cd example-widget-loader && yarn init -y
or if you use npm:
$ mkdir example-widget-loader && cd example-widget-loader && npm init -y
For convenience let’s install a package that will allow us to serve a test page with the loader.
$ yarn add serve
Let’s add the script serving the page to the package.jsonOpen link in new window file:
9"scripts": {10 "start": "serve ./"11 }
Creating a loader
The loader.js file will be the main and only file of our loader.
First of all, let’s close all the code in IIFE (immediately invoked function expression) so as not to pollute the global namespace by our code.
File loader.jsOpen link in new window :
1(() => {2 3})();
All the code will be placed inside this function.
Then, we get a reference to the HTML element that just added our script to the page.
3const script = document.currentScript;
Important! document.currentScript property will not return the correct value if we get it inside a callback. Therefore, the returned reference must be assigned to a variable directly in the script.
Now let’s move on to the loading function.
For the convenience of styling, the iframe will be placed in div. For the sake of simplicity, we assume the widget will be displayed in the upper right corner of the browser window. We will put it there with absolute positioning.
5const loadWidget = () => {6 7const widget= document.createElement("div");8
9const widgetStyle = widget.style;10widgetStyle.display = "none";11widgetStyle.boxSizing = "border-box";12widgetStyle.width = "400px";13widgetStyle.height = "647px";14widgetStyle.position = "absolute";15widgetStyle.top = "40px";16widgetStyle.right = "40px";
Note that the widgets display property is set to “none” by default. This is because we only want to display it after it’s loaded.
Now, let’s create an iframe:
18const iframe = document.createElement("iframe");
and let’s set some basic attributes:
20const iframeStyle = iframe.style;21iframeStyle.boxSizing = "borderBox";22iframeStyle.position = "absolute";23iframeStyle.right = 0;24iframeStyle.top = 0;25iframeStyle.width = "100%";26iframeStyle.height = "100%";27iframeStyle.border = 0;28iframeStyle.margin = 0;29iframeStyle.padding = 0;30iframeStyle.width = "500px";
The iframe is ready. Now we can add it to the container:
32widget.appendChild(iframe);
It may take a while for the widget to be loaded into the frame. We don’t want the widget to be visible until it’s loaded. Therefore, we will show it only when everything is ready. To achieve this, let’s add a callback onload to the iframe, in which we change the widget’s display property to “block”.
34iframe.addEventListener("load", () => widgetStyle.display = "block" );
Finally, we load the widget into the iframe.
36const widgetUrl = `http://localhost:3000`;37 38iframe.src = widgetUrl;
and add it to the page:
40document.body.appendChild(widget);41
42}
Since we don’t want the widget loading to block the page, we added the async attribute to the script tag. We also want to make sure that the loading function will not execute until the entire page has been loaded.
Here is a small trap: We don’t know when exactly the script will run. It is possible that it will happen after the DOMContentLoaded event, so first, we need to check the loading state of the document using the document.readyState property.
If the document is loaded we run the loading function, and if not yet, we add an event listener for the DOMContentLoaded event to the window object. The event handler starts loading the widget.
44if ( document.readyState === "complete" ) {45 loadWidget();46} else {47 document.addEventListener("readystatechange", () => {48 if ( document.readyState === "complete" ) {49 loadWidget();50 }51 });52}
Our first version of the loader is ready!
Now it’s time to test it. Let’s create a page where we will load the widget using a loader.
Test page
File index.htmlOpen link in new window :
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="utf-8" />5 <title>Widget loader test</title>6 <style>7 html,body{8 width:100%;9 height:100%;10 margin:0;11 padding:0;12 background-color: darkslategrey;13 color: #fff;14 }15 </style>16 <script async src="loader.js" data-license="123"></script>17</head>18<body>19<div style="padding:2em">20 <p>Widget example page</p>21</div>22</body>23</html>
Run the widget from the root of the project with the command:
$ yarn run start
or if you use npm:
$ npm run start
Run the same command in the root of the loader project.
The server for the widget will be started on port 3000 and the loader server on port 5000.
Go to http://localhost:5000 in a browser. If you haven’t made any mistakes, the widget should be loaded!
So far so good. The widget has been loaded and there is not a single line of javascript in the tag script. The widget code is separate from the page’s code and the page itself is not interfered with.
Now let’s try to use the license parameter, which was passed using the data-license attribute of the script tag (we’ll deal with the data-greeting parameter later).
Get the license
Branch: stage-2-2
$ git checkout stage-2-2
In the loader, we get the value of the data-license attribute.
File loader.jsOpen link in new window :
36const license = script.getAttribute("data-license");
Then, we append the value of the license as a parameter in the query string to the widget url.
37const widgetUrl = `http://localhost:3000?license=${license}`;
Now we need to go back to the widget’s code and get the parameter from the URL.
Branch: stage-2-2
$ git checkout stage-2-2
Add the following code to the src/App.jsOpen link in new window file:
1import { useState, useEffect} from "react";
7const [license, setLicense] = useState("");8 9useEffect( () => {10const queryString = window.location.search;11const urlParams = new URLSearchParams(queryString); // doesn't work in IE, but who cares ;)12const license = urlParams.get("license");13setLicense(license);14},[]);
Using the useEffect hook where the second parameter is set to an empty array, will guarantee that the license retrieval from the parameter in the url will only be performed once during the first rendering of the component. After obtaining the license value, we set it using the setLicense function from the setState hook, which will re-render the component.
Pass the license to the widget container
17<WidgetContainer license={license} />
We will use the license number to change the name of the company we chat with visible in the widget header.
In a real application, of course, the license would be sent to the server, and relevant data would be generated there. However, for the purposes of this post, we will simplify things a little. We’re just going to hardcode our settings.
Add the following snippet In the src/WidgetContainer.jsxOpen link in new window file:
1import { useState, useEffect, useMemo } from "react";
20const remoteName = useMemo( () => {21 if ( license === "123" ) {22 return "Chatscope";23 } else if (license === "456" ) {24 return "ChatKitty";25 } else if (license === "789" ) {26 return "EvilNull";27 }28},[license]);
Pass the remoteName to the widget:
48return <Widget remoteName={remoteName} messages={messages} onSend={handleSend} />
Now, you can check that the parameter passing works, by running the test application and changing the license string. A name depending on the license passed in the script argument should appear in the chat header. Check if each of them works.
Other examples of attributes that we can pass in this way:
- Widget API configuration (we will deal with this in the third part of the series),
- Selector where the widget should be placed on the page (e.g. id of the element to which the widget is to be injected),
- The color theme or the leading color of the widget,
- Any other configuration parameters, that may be useful for the specific widget
Another way to pass parameters to the widget
Passing parameters in a URL is simple and it works. However, there is another - a cooler way that I want to show you. It will be a bit more complicated but the knowledge gained will be useful for building the API later.
We will use the postMessage method for this.
Do you remember the data-greeting attribute of the script tag, which we left for later? It’s time to take advantage of it.
Branch: stage-2-3
$ git checkout stage-2-3
Let’s start with the loader. First, we get the value of the attribute from the script tag, just like we did for the data-license attribute.
File loader.jsOpen link in new window :
34const greeting = script.getAttribute("data-greeting");
Then we modify the onload iframe handler. We will send a greeting object to the frame here.
36iframe.addEventListener("load", () => {37 iframe.contentWindow.postMessage({greeting}, "http://localhost:3000");38 widgetStyle.display = "block";39});
Now the message has to be picked up in the widget.
Branch: stage-2-3
$ git checkout stage-2-3
In the src/App.jsOpen link in new window file add the state variable for the greeting content:
8const [greeting, setGreeting] = useState();
…and a hook where we will add an event listener to the message event of the window object:
17useEffect(() => {18
19 const handleMessage = evt => {20 if ( "greeting" in evt.data ) {21 setGreeting(evt.data.greeting); 22 }23 };24
25 window.addEventListener("message", handleMessage);26
27 return () => window.removeEventListener("message", handleMessage);28
29}, [setGreeting]);
The hook works as follows: When the window receives a message containing the greeting property, we set the state value to the content of the greeting and pass it to the widget:
31<WidgetContainer license={license} greeting={greeting} />
The rest, that is displaying the welcome message in the chat window is done by useEffect in the Widget component, which we wrote earlier. As a reminder: It adds a message to the array if the greeting property is empty.
Attention! In a real application, the handleMessage function should check that the message received is from a trusted domain. For the sake of simplicity, we omit this. I will write more about this in the third part of the series.
Done! After refreshing the test page, a welcome message should appear in the chat window!
Do it yourself
The loader can be expanded in various ways. If you want to learn more, try adding the following features yourself:
- The ability to configure the element to which the widget will be inserted on the page, so that the user embedding the widget can decide for himself, where exactly the widget will be placed (add the data-container attribute to the script tag)
- Configuration of the CSS class to be added to the main container or widget iframe (add the data-class attribute to the script tag)
- Delayed display of the widget: add the data-showdelay attribute that will define the time after which the widget will be displayed on the page counting from its loading.
- Animation of loading or displaying the widget - come up with something nice yourself that will attract the attention of users
Summary
You have successfully reached the end of the second post in the series.
I’m proud of you for making your way through this long text!
Let’s summarize the acquired knowledge:
- You already know, how to build a widget
- You know how to embed a widget on the website in an elegant way
- You can pass parameters from the script tag to the loader and widget
- You got examples of expanding the loader that you can use for the further learning
If the topic interests you, stay with me and wait for the next post where we will build an API for widgets. These will be the most interesting issues in the entire series!
I also encourage you to experiment - try to implement the features from the ”Do it yourself” chapter - it’s best to learn programming by practice.
Thank you for your perseverance!
Continue reading
- Web widgets (Part 1): What is it?
- Web widgets (Part 3): API Cookbook!
- Web widgets (Bonus): Why iframe?
Share
If you find this post valuable, please share it. Knowledge should be shared!