Web Components are a great way to build reusable functionality you can use in different web pages and web apps. Imagine sharing components between different frameworks like React, Vue.js, or Next.js! In this post, we delve into Web Components and show you how to build a chat web component with Ably to use it in an application built with AWS Amplify and AWS Lambda.
Web Components are a collection of web technologies, standardized in the browser, to ease writing reusable pieces of markup and functionality. They’re a combination of Custom Elements, the Shadow DOM and HTML templates. When they’re used all together, they’re collectively called Web Components.
Web Components are an alternative to framework-specific approaches to component reuse such as React Components, and Vue.js templates. Web Components are interesting because they’re not bound to any runtime framework, and they’re supported by all modern browsers. Competing approaches to components for the web were established prior to Web Components being widely supported, which caused a lot of the frameworks to develop their own approaches to reuse and encapsulation. Web components offer a framework-independent way of building reusable functionality for the browser.
When talking about Web Components, most people refer to Custom HTML Elements, as these are the closest thing to the component models found in React or Vue. Custom Elements can:
- Render HTML into themselves by setting their own innerHTML properties.
- Encapsulate data by storing it as attributes on instances of themselves.
- Track changes to data stored in their attributes, and trigger a re-render.
- Allow you to write code that is triggered when they are added and removed from the DOM.
CountComponent. This will extend HTMLElement (the browser's base type for all its elements):
Inside this class, we need to define the attributes to be observed for
change tracking. For the sake of this demo, we will return an array of a single string – the attribute name
Next, we need to define some custom attributes and their behaviour. We create a
getterfor the custom “count” attribute, and in the function, the attribute value
count is loaded with a call to
getAttribute, defaulting to the string
"0". Once the data is loaded, we’re parsing it from JSON – we do this because any data that we store must be a string.
We also need to create a setter for our attribute, and here we’re serializing the value to JSON and setting it on our element:
Even though Custom Elements are defined as classes, you can’t put any logic inside their constructors other than calling
super to trigger the base logic for the HTML Element class. Add any other logic, or miss out calling super and you’ll see an error in your console and the element won’t work.
Because we can’t do any meaningful work in the constructor, Custom Elements provide lifecycle callbacks – functions that you can implement at different parts of the component lifecycle. We will use the
connectedCallback function. Implementing this callback will cause the code to run when the component is added to the DOM – it is similar to React's
componentDidMount function. In connectedCallback, we’re setting a default for the counter, and calling a function we’ll look at shortly called
The next lifecycle callback function available to us is
disconnectedCallback (shown here only for illustration purposes as we’re not running any code). The code in this function executes when your element is removed from the DOM and destroyed.
Custom Elements also provide us with an
attributeChangedCallback. We will use this function to execute code whenever an
setContent is a function that the component calls when the component is added to the DOM, and when an update happens. The click handler in the above code can access the attributes of the element using the
this keyword, so on each click, we increment the value, in turn triggering a re-render.
Note: This demo is not particularly performance focused, in that it sets the entire innerHTML and wires up a click handler to the button each time it’s called. This is not how one would build an application for production! In a real application, you would not reset your entire DOM. Instead you would only change the parts of the UI that required updating. Furthermore, the Shadow DOM APIs could be used in more performance-sensitive scenarios.
And the component renders thus
We're going to build a text chat component – Ably Chat component – by building two Custom Elements:
- AblyBaseComponent.js – a base component that encapsulates Ably interactions
- AblyChatComponent.js – an element that inherits from AblyBaseComponent and implements chat logic on top of Ably.
The base component is designed strictly to be built upon, and to encapsulate the basic requirements for Ably API key management and subscribing to channels. Using this base component, we could build any kind of Custom Element that relies on realtime messaging.
The base component starts with the standard Custom Element boilerplate – AblyBaseComponent extends HTMLElement, and calls its constructor.
In the connectedCallback function, we call
this.connectToAblyChannel (which we’ll examine shortly):
The disconnectedCallback function loops through
this.subscribedChannels (which we will create in a
connect function) and unsubscribes from any Ably channels that have been used:
connectToAblyChannel function is where a lot of the real work happens. We start off by loading some configuration – we will set up a convention that we expect users to supply either their Ably API key, or a callback URL to an API that responds with an Ably token authentication request. To supply these values, and because we’re creating HTML elements, we expect them to be set as attributes on the element when it’s created in the markup.
In order to read the data attribute when the element is connected, we expect the element to look as follows:
This code tries to load
Once we have the SDK instance, stored in the variable
this.ably, we will also create an empty array called
We’re going to conclude with two additional functions in the AblyBaseComponent – a
publish function and a
The regular Ably SDK exposes publish and subscribe functions once you have called
ably.channels.get(channelName)to get a channel. For this chat example, we want to let the AblyChatComponent decide which channels it will publish and subscribe on, effectively extending the API surface area of the Ably SDK (the set of things the API can do).
The augmented publish and subscribe functions take an extra parameter at the start – the channel name – then pass on the rest of the arguments to the Ably SDK to do all the hard work for us. We do this by destructuring the arguments array, and ignoring the first element. This allows us to capture all of the parameters, except the first one, in our newly defined variable
We can then use
.apply on the publish or subscribe calls from the Ably SDK to pass the rest of the variables to the SDK to publish or subscribe to messages.
We will use the first parameter, the
channelName, to get the correct channel and keep track of it so we can unsubscribe when we unmount from the DOM. The following code will go inside the
publish function, below the arguments assignment. The subscribe function works similarly
As you can see, both
subscribe are virtually identical functions – with the exception that we’re passing to them the call to
This all might seem a little framework-like, but what it means is that developers building Ably components on top of this base class can call
subscribe with an extra
channelName parameter at the start of the call, and not worry about connecting or disconnecting from Ably channels.
This base class contains all our Ably code, and is now ready for us to build exciting components on top of it, so let’s create our second component, the AblyChatComponent.
These components are designed to be imported as an ES6 Module – where the script tag you use in your HTML has
type="module"as an attribute. When using ES6 modules, we can use
import in browser components, so to start here, we’re importing our AblyBaseComponent in order to extend it.
The next thing to do is set up some boilerplate code for the element. Define an attribute called “messages”, and set it up to be observable. Next, set up
constructor, which in turn calls the constructor of the AblyBaseComponent using a call to
We can use
connectedCallback to configure the chat application. We call
super.connectedCallback to trigger all of the Ably configuration logic from the base component. The following code goes below
constructor, inside the closing bracket of the
We call the
super.subscribe* *function that was defined on the base to subscribe to messages on a channel called
chat-message. When a message is received, we call a function called
this.onAblyMessageReceived (which we’ll implement in a moment), passing the received message as an argument.
In order to ensure that any CSS styles applied to the page don't affect the component, and vice versa, inside the body of
connectedCallback, we will generate a random string and assign it to a property called
Next we call a function named
renderTemplateAndRegisterClickHandlers, which we’ll look at shortly.
Finally, we place the browser focus on an element called
this.inputBox that is generated when the template is rendered, so that people using the chat UI will be able to instantly start typing.
Next we use the
attributeChangedCallback to update the innerHTML of the chat bubbles in the chat window when messages are received. When an attribute is changed, the
this.chatText is set and scrolled into view. We’re using a function called
formatMessages, which takes the message history, and converts it to HTML elements fit for display:
Next we set up the
renderTemplateAndRegisterClickHandlers function, which is named for what it does! The start of this function calls another function called
defaultMarkup, that takes a single parameter – the *id *of the element, and returns the innerHTML we want to display on the screen – an empty chat box element.
Once the element has been rendered into the DOM, we can use
querySelectorAll to find the chatText, inputBox, sendButton, and messageEnd elements so that we can use them in our code:
Inside we also wire up the eventListeners for clicks on the sendButton and on each keyPress in the input box so that we can process user input.
We mentioned the
onAblyMessageReceived function earlier when we subscribed to Ably messages – this function takes the current message history from
this.messages, makes sure it is at most 199 messages long, and adds the latest message to the end of the array:
This new array is then assigned to
this.messages, which triggers the UI to re-render because
this.messages is an observed property.
sendChatMessage function is called either when Enter is pressed, or when the button to send a message is clicked. Because we’re extending the AblyBaseComponent, it calls the
super.publish function, passing the channel name and the Ably message payload:
You can see that it’s also responsible for clearing the text box and focusing on it so the user can continue chatting.
handleKeyPress function is triggered on each keypress. If the keypress is the Enter key and there is a message in the chat box, it sends the chat message:
formatMessages function is responsible for mapping the Ably message history into span elements. There’s a little bit of logic here to detect whether or not the message was sent by the current user of the app by checking the
message.connectionId property against the
ably.connection.id property, and adding a
other CSS class that can apply styles to. The
data property from the message is used to carry the received text message.
The above concludes the custom element class. After the custom element class ends, we have two functions. The first is the
uuidv4() function – which generates a unique
id for the component:
The second is
defaultMarkup which takes a single parameter – the ID of the component – and uses it to set the
id property on the generated HTML.
Once we’ve set this
id property, we can embed CSS that is scoped specifically to this element ID directly into the output code. This means that if multiple instances of the custom element appear on the same page, they won’t have conflicting ids or styles.
At the bottom of this next snippet you can see the markup for the component – a
div for holding message history, and a
form for capturing user input, complete with the class names used in our query selector calls earlier.
And of course, much like our example element at the start, we’re calling
customElements.define to register the HTML tag:
The custom element is now functionally complete, and so long as both the AblyBaseComponent.js and AblyChatComponent.js files are included in a web application, they can be used by referencing our AblyChatComponent.js as a module.
To use the now-registered custom element, we use it like any old HTML tag and provide it with the correct attributes:
As hinted above, we need to talk about API key management. While this custom element supports reading Ably API keys straight from your markup – which is great for local development and debugging – you absolutely should not store your Ably API keys in your markup, otherwise they could get stolen and misused.
The recommended way to use API keys in the front-end is to use Ably Token Authentication. Token authentication is an exchange mechanism where you use your real API key to generate limited-use tokens that can be passed back to your clients to use in the front end.
Ably.Realtime client passing your Ably API key from process.env.ABLY_API_KEY.
client.auth.createTokenRequest to generate a temporary token, and return it back to the client.
The onus is on the owner of the API key to make sure that the users requesting a temporary token have access to chat – you can authenticate the requests any way you’d like, and it’s no different from authentication in any other lambda function. In the next section, we go through hosting this on AWS Lambda
To deploy to AWS Lambda, we need to create a new directory called /api/createTokenRequest with two files in it – package.json and index.js Here’s the package.json file
And this is the index.js file
These two files, along with their node_modules are required by the AWS Lambda runtime. We're going to use npm to restore the node modules and then compress the contents of the /createTokenRequest directory into a zip file. In the terminal execute the following:
After that, zip up the contents of the createTokenRequest directory (this process is OS dependent). We will use the AWS UI to create a Lambda function and upload this zip file as the source code.
We'll walk through this process now. You need to log in to your AWS account first:
- Search for lambda in the Services search bar, and click the Lambda services box that shows up in the results:
- Click the “Create function” button to create a new Lambda function
- Select "Author from scratch" and give your function a name, then click "Create function":
- Once the function has been created, click “Add trigger” to make the Lambda function accessible over HTTP:
- Select “API Gateway” from the “Trigger configuration” dropdown
- On the resulting page, select your function from the dropdown and click "Add".
- Set the Deployment stage to default and the Security to Open, then click "Add".
- Once you have added your trigger, the UI shows a URL in the Triggers tab under Configuration. (This is what you will add as the
data-get-token-urlparameter when you use your component in the HTML, but we still have some more setup to do!)
- Now you need to upload the zip file that we created earlier. Click on the "Code" tab, then "Upload from" and select ".zip file":
- Once the zip file is uploaded, you will need to set up your environment variables with your Ably API key. Under "Configuration", select "Environment variables", then click the "Edit" button: Add your Ably API key to the “Environment variables” settings And that's it, your Lambda is set up
And that's it, your Lambda is set up
In the index.js file we’re reading from
process.env.ABLY_API_KEY. You will need to generate a new ably API key and then define this environment variable, with it’s key value in the AWS UI (or using an automation tool of your preference).
Once our Lambda function is created, we’ll need to add an AWS API Gateway Trigger to give our lambda an externally accessible URL. This is the URL that we can safely configure in our HTML markup in lieu of our actual API key. The Ably SDK will take care of the rest.
Now we can walk through hosting your component on Amplify, the AWS static web hosting service.
Here at Ably, we’ve published the component to NPM as ably-chat-component, and you can reference it directly using the Skypack CDN. This ensures packages are browser-compatible.
You need to reference the client-side Ably SDK for the component to work. However, once you have done that, you can reference the Skypack URL for our component, and add the ably-chat tag into your page, set your API key, and everything will just work.
This is the simplest supported way to use this component in development mode. As mentioned above, however, you will need to switch out your API key for a token request URL of your own.
In this piece we’ve broken down how web components work, explored Ably and Web Components, and walked through how we can use AWS Amplify and AWS Lambda to host applications that support realtime chat.
If you’ve already got a web application and know how to host it, we’ve also touched on how you can use Skypack to include this component directly from NPM.
Chat is just one way that you can use realtime messaging and Web Components, and we’d love to see what you can do with this codebase.