Day 27: Building an MVP – Tooltips, Shadow DOM, and Web Components

On Day 25, we connected the essential flow of our application: creating a tooltip. The second most important part of the Knowlo application is loading tooltips on users’ websites and web apps. What’s the best approach for loading and displaying tooltips? Let’s ask CofounderGPT!
Loading Tooltips
Slobodan: What would be the best way to create a JavaScript library that users can embed in their websites or web apps to connect to Knowlo API and show our smart tooltips? This script can work similarly to Google Analytics script or Sentry script. But the styles of our tooltips need to be isolated from clients’ CSS.
CofounderGPT: To build such a JavaScript library, you can follow a structured process. Here’s an outline for a method that could work well for this:
- Determine the API Structure: You need to have a solid understanding of the Knowlo API that your library will interact with. This includes understanding the different endpoints, request/response formats, and any authentication required.
- Design the Client-side Library: Design a library that’s easy to use and integrate. This involves designing the public interface that users will interact with. For example, if the user wants to initialize the tooltip on a certain element, they might call something like
Knowlo.init(element, options)
. - Initialize Knowlo: Create a constructor function or class that represents a Knowlo instance. This instance can manage state and perform setup when initialized. It might accept an API key for interacting with the Knowlo API.
- Communication with the Knowlo API: Create functions for communicating with the Knowlo API. For example, you might have a method to fetch the user’s knowledge base or to send a query to the API.
- Rendering Tooltips: When the user wants to show a tooltip, your library needs to dynamically create the tooltip element and append it to the DOM. You’ll use JavaScript to set the content of the tooltip based on the data from the Knowlo API. To isolate styles from the client’s CSS, you could use Shadow DOM or CSS-in-JS solutions.
- Handling User Input: When the user asks a question, you’ll need to handle this input, send it to the Knowlo API, and update the tooltip with the new information.
- Testing: Make sure to test your library thoroughly. You can use tools like Jest for unit testing and Puppeteer for end-to-end testing.
Here’s a simple example of what the JavaScript library might look like:
class Knowlo {
constructor(apiKey) {
this.apiKey = apiKey;
}
async fetchKnowledgeBase() {
// Use fetch or axios to call the Knowlo API and get the knowledge base
}
createTooltip(element, content) {
// Create a shadow root to isolate styles
let shadow = element.attachShadow({mode: 'open'});
// Create the tooltip element
let tooltip = document.createElement('div');
tooltip.textContent = content;
tooltip.style /* set your styles here */;
// Append the tooltip to the shadow root
shadow.appendChild(tooltip);
}
async showTooltip(element, feature) {
// Fetch the feature info from the knowledge base
let featureInfo = await this.fetchKnowledgeBase(feature);
// Show the tooltip on the element
this.createTooltip(element, featureInfo);
}
// More methods here for handling user questions and interactions...
}
// To use the library, the user would do something like this:
let knowlo = new Knowlo('my-api-key');
knowlo.showTooltip(myElement, 'my-feature');
Remember to package your library correctly so it can be included on a webpage with a script tag or used with a module bundler like Webpack.
Before releasing, remember to minify the JavaScript for optimal load times. Tools like Terser can help with this.
Also consider providing TypeScript definitions to improve the developer experience for those using TypeScript.
Slobodan: Ok, this makes sense. Ideally, users should just import our JavaScript file (i.e., https://cdn.knowlo.io/v1/script.js), set their project ID, and then attach some data attribute (i.e., data-knowlo-tooltip-id="123"
) to elements that should show a specific tooltip.
CofounderGPT: Yes, this would provide an excellent developer experience, making it really easy to integrate your tool into any web application.
The following example demonstrates how you can modify the library for this:
1. Importing the JavaScript File:
<script src="https://cdn.knowlo.io/v1/script.js"></script>
2. Setting the Project ID:
<script>
Knowlo.setProjectId('my-project-id');
</script>
3. Attaching a Data Attribute:
<button data-knowlo-tooltip-id="123">Click me</button>
Now, to make this work, you’d have to modify your Knowlo class as follows:
class Knowlo {
constructor() {
this.projectId = null;
}
setProjectId(projectId) {
this.projectId = projectId;
}
async fetchTooltip(id) {
// Use fetch or axios to call the Knowlo API and get the tooltip
}
createTooltip(element, content) {
// Same as before...
}
async attachTooltips() {
// Get all elements with a 'data-knowlo-tooltip-id' attribute
let elements = document.querySelectorAll('[data-knowlo-tooltip-id]');
for (let element of elements) {
// Get the tooltip ID
let id = element.getAttribute('data-knowlo-tooltip-id');
// Fetch the tooltip info
let tooltipInfo = await this.fetchTooltip(id);
// Attach the tooltip to the element
this.createTooltip(element, tooltipInfo);
}
}
}
// Make an instance of Knowlo available globally
window.Knowlo = new Knowlo();
Then, in your script.js
, you would call Knowlo.attachTooltips()
once the DOM is fully loaded. This can be done by adding an event listener for the DOMContentLoaded
event:
document.addEventListener("DOMContentLoaded", function() {
Knowlo.attachTooltips();
});
This setup will automatically attach tooltips to all elements with a data-knowlo-tooltip-id
attribute once the DOM is loaded.
Please be aware that you need to handle potential rate limits or bulk requests to your API while fetching tooltip data. You may want to think about adding a local cache or some other type of rate limiting to prevent unnecessary API calls and improve the user experience.
Shadow DOM is the browser’s ability to add a subtree of DOM elements to the document instead of adding them to the main DOM. This approach is very interesting because of the Shadow DOM isolation. For example, user styles will not affect Knowlo styles unless we want them to, etc.
Slobodan: Ok, this is an interesting approach. You mentioned using a Shadow DOM. That’s an excellent idea. A friend of mine recommended using Web Components. Can we use Web Components and make a custom element for each tooltip?
Thanks, Ivan! The concept of Web Components is one of the interesting technologies that promised a lot but never fully took off. The idea is to have a built-in suite that allows us to create custom HTML elements. People argued that Web Components could kill popular single-page application frameworks. It took Web Components a lot of time to mature and reach most modern browsers, and it seems that their popularity has faded slightly (I might be wrong). However, Web Components are still a powerful way of writing custom HTML elements without using some complicated front end frameworks.
CofounderGPT: Absolutely, using Web Components is an excellent idea, especially for your use case. With Web Components, you can encapsulate the logic and styles for your tooltips into a custom HTML element.
Here’s how you might define a Web Component for a tooltip:
class KnowloTooltip extends HTMLElement {
constructor() {
super();
this._tooltipId = null;
this._tooltipContent = null;
// Create a shadow root
this._shadowRoot = this.attachShadow({ mode: 'open' });
// Define the styles for your tooltip here. This style is encapsulated within the shadow DOM and won't interfere with your host document's styles
this._styles = `
/* Styles go here */
`;
// Insert styles into the shadow root
const styleElem = document.createElement('style');
styleElem.textContent = this._styles;
this._shadowRoot.appendChild(styleElem);
}
connectedCallback() {
// When the custom element is inserted into the DOM...
this._tooltipId = this.getAttribute('data-knowlo-tooltip-id');
this.fetchTooltip(this._tooltipId).then((content) => {
this._tooltipContent = content;
this.render();
});
}
render() {
// Render the tooltip with its content
const tooltipElem = document.createElement('div');
tooltipElem.textContent = this._tooltipContent;
// Append the tooltip to the shadow root
this._shadowRoot.appendChild(tooltipElem);
}
async fetchTooltip(id) {
// Use fetch or axios to call the Knowlo API and get the tooltip
}
}
// Define the custom element
customElements.define('knowlo-tooltip', KnowloTooltip);
Then, users of your library can simply add tooltips to their site by adding a knowlo-tooltip
element with a data-knowlo-tooltip-id
attribute:
<knowlo-tooltip data-knowlo-tooltip-id="123"></knowlo-tooltip>
This custom element approach will give users of your library a lot of flexibility because they can add tooltips simply by adding HTML elements to their site. It also keeps your library’s JavaScript and CSS from interfering with the host site’s JavaScript and CSS. And if they need to update the tooltip ID, they can just update the data-knowlo-tooltip-id
attribute, and the tooltip content will be automatically updated.
Slobodan: I like this approach. How do they set the project ID when initializing our script? I do not want to require users to pass a project ID to each tooltip.
CofounderGPT: It’s definitely possible to set a project ID globally rather than per tooltip. You can set a project ID in a global scope that all instances of KnowloTooltip
can access. You can do this by adding a method to the window
object that allows setting the project ID. Here’s how:
window.Knowlo = {
projectId: null,
setProjectId(id) {
this.projectId = id;
}
};
class KnowloTooltip extends HTMLElement {
// ...
connectedCallback() {
// ...
// When the custom element is inserted into the DOM...
this._tooltipId = this.getAttribute('data-knowlo-tooltip-id');
this._projectId = window.Knowlo.projectId;
this.fetchTooltip(this._projectId, this._tooltipId).then((content) => {
this._tooltipContent = content;
this.render();
});
}
async fetchTooltip(projectId, id) {
// Use fetch or axios to call the Knowlo API and get the tooltip
}
// ...
}
customElements.define('knowlo-tooltip', KnowloTooltip);
Then the users of your library can set the project ID like so:
<script>
Knowlo.setProjectId('my-project-id');
</script>
All instances of KnowloTooltip
will have access to the projectId
set in the window.Knowlo
object. You would typically call Knowlo.setProjectId('my-project-id')
in a script tag in your HTML file after including the script.js
file. Make sure to call this before any knowlo-tooltip
elements are defined in the HTML.
Slobodan: I don’t like adding global variables. Can we use JavaScript closures or something else that would remove the requirements for global variables?
CofounderGPT: Sure, you can use JavaScript closures to encapsulate the projectId
and remove it from the global scope. Closures allow a function to access variables from an outer function after the outer function has finished its execution.
A good way to utilize closures in this case is to wrap your entire library in a function that’s immediately invoked (an IIFE – Immediately Invoked Function Expression). Here’s how you might do it:
(function() {
let projectId = null;
// Expose a function to set the project ID
window.setKnowloProjectId = function(id) {
projectId = id;
};
class KnowloTooltip extends HTMLElement {
// ...
connectedCallback() {
// ...
this._tooltipId = this.getAttribute('data-knowlo-tooltip-id');
this._projectId = projectId;
this.fetchTooltip(this._projectId, this._tooltipId).then((content) => {
this._tooltipContent = content;
this.render();
});
}
async fetchTooltip(projectId, id) {
// Use fetch or axios to call the Knowlo API and get the tooltip
}
// ...
}
customElements.define('knowlo-tooltip', KnowloTooltip);
})();
Now, projectId
is not a global variable but it’s accessible from within the setKnowloProjectId
function and the KnowloTooltip
class due to the closure. The users of your library can set the project ID like so:
<script>
setKnowloProjectId('my-project-id');
</script>
This removes the Knowlo
object from the global scope and replaces it with a setKnowloProjectId
function.
Slobodan: Is it possible to pass a project ID as a script query parameter or a data attribute?
CofounderGPT: Absolutely! Both methods can be used to pass a project ID when loading the script.
1. Using a script query parameter:
To use a query parameter, the user would load your script like this:
<script src="https://cdn.knowlo.io/v1/script.js?projectId=my-project-id"></script>
In your script, you would then parse the URL of the script tag to get the projectId
:
(function() {
// ...
function getProjectId() {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
if (script.src.includes('https://cdn.knowlo.io/v1/script.js')) {
const url = new URL(script.src);
return url.searchParams.get('projectId');
}
}
}
const projectId = getProjectId();
// ...
})();
2. Using a data attribute:
To use a data attribute, the user would load your script like this:
<script src="https://cdn.knowlo.io/v1/script.js" data-project-id="my-project-id"></script>
In your script, you would then get the data-project-id
attribute from the script tag:
(function() {
// ...
function getProjectId() {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
if (script.src.includes('https://cdn.knowlo.io/v1/script.js')) {
return script.getAttribute('data-project-id');
}
}
}
const projectId = getProjectId();
// ...
})();
These methods allow you to get the projectId
when the script is loaded, and you don’t need to expose any global functions.
Slobodan: Ok, the data attribute approach seems more clear to me. Is it allowed by HTML5 specification? Can we just do a for loop on script elements backward if it is? There’s a big chance that the Knowlo script will be the last one imported on the page at the moment running the setup function.
CofounderGPT: Yes, using data attributes is absolutely allowed and even recommended by the HTML5 specification. Data attributes are a great way to add extra information to elements that don’t have any associated semantics in HTML.
In addition, iterating over script tags in reverse order to find the last inserted script tag is also a great idea, and it is perfectly allowed. In most cases, your script will indeed be the last one, especially if it’s inserted right before the closing body tag as recommended.
Here’s how you can modify the getProjectId
function to iterate in reverse order:
function getProjectId() {
const scripts = document.getElementsByTagName('script');
for (let i = scripts.length - 1; i >= 0; i--) {
const script = scripts[i];
if (script.src.includes('https://cdn.knowlo.io/v1/script.js')) {
return script.getAttribute('data-project-id');
}
}
}
This will start from the last inserted script tag and go backwards, which should be more efficient if the script is indeed the last one. This would be the correct way to handle this situation if you want to avoid any global scope pollution.
Writing Web Components
I started a new chat with CofounderGPT and asked it to help me write a basic web component for Knowlo tooltips. I have never before used Web Components for anything serious, so CofounderGPT can help me to get started.
Slobodan: Can you help me to write a web component that shows a tooltip? I need a custom component, such as <knowlo-tooltip id="some-id"><!-- wrap HTML inside --></knowlo-tooltip>
, where users can use my component as a wrapper around their button or some other element. Once someone clicks on the <knowlo-tooltip/>
component, I need to show a tooltip with some content.
CofounderGPT: Sure, you can use the Web Components technology built into modern browsers. We will use vanilla JavaScript to create a custom element.
Let’s create a new JavaScript file, knowlo-tooltip.js
:
class KnowloTooltip extends HTMLElement {
constructor() {
super();
this._tooltipIcon;
this._tooltipVisible = false;
this._tooltipText = "Some dummy tooltip text.";
this.attachShadow({ mode: "open" });
}
connectedCallback() {
if (this.hasAttribute("text")) {
this._tooltipText = this.getAttribute("text");
}
this._tooltipIcon = document.createElement("span");
this._tooltipIcon.textContent = " (?)";
this.shadowRoot.appendChild(this._tooltipIcon);
this._render();
this._tooltipIcon.addEventListener("click", this._toggleTooltip.bind(this));
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name === "text") {
this._tooltipText = newValue;
}
}
static get observedAttributes() {
return ["text"];
}
_render() {
let tooltipContainer = this.shadowRoot.querySelector("div");
if (this._tooltipVisible) {
tooltipContainer = document.createElement("div");
tooltipContainer.textContent = this._tooltipText;
this.shadowRoot.appendChild(tooltipContainer);
} else if (tooltipContainer) {
this.shadowRoot.removeChild(tooltipContainer);
}
}
_toggleTooltip() {
this._tooltipVisible = !this._tooltipVisible;
this._render();
}
}
window.customElements.define("knowlo-tooltip", KnowloTooltip);
In this code:
- We first create a class
KnowloTooltip
that extends fromHTMLElement
, which is a browser built-in object representing any HTML element. - In the
constructor
, we define and initialize our internal state. connectedCallback
is a lifecycle method that’s called when the element is attached to the DOM. Here we initialize the tooltip text and listen for clicks to show or hide the tooltip.attributeChangedCallback
is another lifecycle method that’s called when an observed attribute has changed. We use this to update the tooltip text when thetext
attribute changes.observedAttributes
is a static getter that returns an array of the attribute names to observe for changes._render
is a method that either shows or hides the tooltip, based on whether it’s currently visible or not.- Finally, we define the new custom element with
window.customElements.define
.
To use the new component, add the script to your HTML and use the new HTML tag like this:
<script src="knowlo-tooltip.js"></script>
<knowlo-tooltip text="This is a tooltip!">
<!-- wrap HTML inside -->
<button>Hover over me!</button>
</knowlo-tooltip>
This example is quite simple, but it should give you a good starting point. You could add more features like positioning the tooltip, customizing its appearance with CSS, etc.
Remember that Web Components are natively supported in most modern browsers, but not in IE. For cross-browser support, you might need to use a polyfill.
Slobodan: Ok thanks, but can we keep the original content and just show a tooltip above or below the content (depending on the element position on the page)?
CofounderGPT: Absolutely, you can modify the custom element to always show the original content and only display the tooltip when the user clicks on the icon. The tooltip can appear either above or below the content depending on where it is positioned on the page.
Here’s how you can modify knowlo-tooltip.js
:
class KnowloTooltip extends HTMLElement {
constructor() {
super();
this._tooltipIcon;
this._tooltipVisible = false;
this._tooltipText = "Some dummy tooltip text.";
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
div {
font-size: 0.8em;
width: 200px;
background-color: black;
color: white;
position: absolute;
z-index: 10;
left: 50%;
transform: translateX(-50%);
padding: 10px;
border-radius: 6px;
box-shadow: 1px 1px 6px rgba(0,0,0,0.26);
opacity: 0;
transition: opacity 0.3s ease-out;
}
:host(.icon-click) div {
opacity: 1;
}
::slotted(*) {
position: relative;
}
span {
background: black;
color: white;
padding: 0.15em 0.5em;
text-align: center;
border-radius: 50%;
cursor: pointer;
}
</style>
<slot></slot>
<span>(?)</span>
<div>${this._tooltipText}</div>
`;
}
connectedCallback() {
if (this.hasAttribute("text")) {
this._tooltipText = this.getAttribute("text");
}
this._tooltipIcon = this.shadowRoot.querySelector('span');
this._tooltipBox = this.shadowRoot.querySelector('div');
this._tooltipIcon.addEventListener("click", this._toggleTooltip.bind(this));
this._tooltipBox.textContent = this._tooltipText;
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name === "text") {
this._tooltipText = newValue;
this._tooltipBox.textContent = this._tooltipText;
}
}
static get observedAttributes() {
return ["text"];
}
_toggleTooltip() {
this._tooltipVisible = !this._tooltipVisible;
if(this._tooltipVisible){
this.classList.add('icon-click');
} else {
this.classList.remove('icon-click');
}
}
}
window.customElements.define("knowlo-tooltip", KnowloTooltip);
This will create a tooltip that appears when you click the icon, and the tooltip will be centered relative to the host element. The tooltip will disappear when you click the icon again.
This script also includes some default styles, but you can customize them to match your site’s look and feel.
To use it, you’d do something like this:
<script src="knowlo-tooltip.js"></script>
<knowlo-tooltip text="This is a tooltip!">
<button>Hover over me!</button>
</knowlo-tooltip>
In this case, the tooltip will appear when you click on the (?) icon, and the text “Hover over me!” will always be visible.
Slobodan: I got the following error:
undefined is not an object (evaluating 'this._tooltipBox.textContent = this._tooltipText')
CofounderGPT: My apologies for the oversight. The error you’re seeing happens because we’re trying to access this._tooltipBox
before it’s defined. The connectedCallback
function where we’re defining this._tooltipBox
runs after the constructor, but we’re trying to set its textContent
within the constructor itself.
We should move the tooltip text initialization to connectedCallback
after this._tooltipBox
is defined.
[…]
After some back and forth, CofounderGPT solved most of the issues. But I was not happy with the results, so I spent some time reading about Web Components and looking at some examples.
One of the excellent starting points for creating a Web Component tooltip was this example: https://web.dev/components-howto-tooltip/.
In less than an hour of reading and experimenting, I had a quick proof of concept with the following code:
const projectId = document
.querySelector("[data-knowlo-project-id]")
.getAttribute("data-knowlo-project-id");
class KnowloTooltip extends HTMLElement {
constructor() {
super();
this.projectId = projectId;
this._show = this._show.bind(this);
this._hide = this._hide.bind(this);
this._hide();
const template = document.createElement("template");
template.innerHTML = `
<style>
@keyframes tooltipFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tooltip:before {
content: "";
display: block;
width: 0;
height: 0;
border-style: solid;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 10px solid #ffffff;
border-top: 0;
transform: translateX(40px);
}
.tooltip {
position: absolute;
display: block;
max-width: 600px;
z-index: 20000;
animation: tooltipFadeIn 0.15s;
font-family: inherit;
}
.tooltip-container {
display: block;
overflow: hidden;
border-radius: 8px;
background: white;
box-shadow: 0px 2px 14px 0px rgba(0,0,0,0.39);
box-sizing: border-box;
}
.tooltip-content {
display: block;
overflow: hidden;
padding: 20px;
width: 100%;
border-radius: 8px 8px 0 0;
background: white;
box-sizing: border-box;
}
.tooltip-footer {
display: block;
overflow: hidden;
padding: 20px;
border-radius: 0 0 8px 8px;
background: #e5e7eb;
}
.tooltip-feedback {
display: block;
}
.tooltip-feedback button {
font-size: 18px;
}
.tooltip-feedback button i {
font-size: 18px;
}
.tooltip-read-more, .tooltip-qna {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.text-sm {
font-size: 0.75rem;
line-height: 1.25rem;
}
</style>
<div class="tooltip" data-knowlo-tooltip>
<div class="tooltip-container">
<div class="tooltip-content">
<div class="tooltip-text">
<p>The accruals feature allows admins to set up the frequency of days accrued based on the yearly leave quota. To enable accruals, follow these steps:</p>
<p>
<ol>
<li>Log in to the online dashboard at app.vacationtracker.io/signin.</li>
<li>Go to Settings and select Locations.</li>
<li>Choose the location you want to add accruals to.</li>
<li>Select the Leave Policies tab.</li>
<li>Click on the Edit icon next to the Leave Type.</li>
<li>In the Accruals section, set the frequency of accrued days. Make sure there is an amount in the Leave Quota section.</li>
<li>Choose one of the three accrual options: Monthly, Semimonthly, or Bi-weekly.</li>
<li>If you select Monthly or Semimonthly, the system will show a breakdown of the accrual period and the next accrual date. If you select Bi-weekly, choose the accrual day.</li>
<li>Click Update to save the accrual settings. Note that once configured, accrual settings cannot be changed.</li>
</ol>
</p>
</div>
<div class="tooltip-feedback float-right text-sm">
Is this information helpful?
<button><i class="fa fa-thumbs-up"></i></button>
<button><i class="fa fa-thumbs-down"></i></button>
</div>
</div>
<div class="tooltip-footer">
<div class="tooltip-read-more">Read more about accruals in Helpdesk <a class="external" href="#">How Do I Set Accruals?</a></div>
<div class="tooltip-qna">
<p>Or ask anything about this feature in this tooltip:</p>
<div class="chat">...</div>
<p class="text-sm float-left">This tooltip is powered by <a href="https://knowlo.co">Knowlo</a></p>
<p class="text-sm float-right">2/2 questions remaining</p>
</div>
</div>
</div>
</div>
`;
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (!this.hasAttribute("role")) this.setAttribute("role", "tooltip");
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", -1);
this._target = document.querySelector(
`[data-knowlo-tooltip-id="${this.id}"]`
);
if (!this._target) return;
this._target.addEventListener("focus", this._show);
this._target.addEventListener("blur", this._hide);
this._target.addEventListener("mouseenter", this._show);
this._target.addEventListener("mouseleave", this._hide);
const tooltipElement = document.querySelector("[data-knowlo-tooltip]");
if (tooltipElement) {
tooltipElement.addEventListener("blur", this._hide);
tooltipElement.addEventListener("mouseleave", this._hide);
}
}
disconnectedCallback() {
if (!this._target) return;
this._target.removeEventListener("focus", this._show);
this._target.removeEventListener("blur", this._hide);
this._target.removeEventListener("mouseenter", this._show);
this._target.removeEventListener("mouseleave", this._hide);
this._target = null;
}
_show() {
this.hidden = false;
}
_hide() {
this.hidden = true;
}
}
window.customElements.define("knowlo-tooltip", KnowloTooltip);
This sample code is far from perfect, but it gave me enough to render a simple tooltip with basic styles. Then I asked CofounderGPT to help me with some improvements, such as keeping the tooltip visible when the user moves the mouse off the target element (i.e., a question mark icon) but still keeping it above the tooltip.
CofounderGPT: The mouseleave
event triggers when the mouse leaves the element, which means that it will trigger even when the mouse goes from the element to the tooltip. In order to prevent the tooltip from closing in such a scenario, you should implement an additional check to see if the mouse is over the tooltip.
In your case, you can attach mouseenter
and mouseleave
event listeners to the tooltip as well, and control its visibility by using a flag. Here’s an idea of how you can modify your component:
class KnowloTooltip extends HTMLElement {
// ... rest of your code
connectedCallback() {
// ... rest of your connectedCallback code
this._tooltip = this.shadowRoot.querySelector('[data-knowlo-tooltip]');
this._tooltip.addEventListener('mouseenter', () => this._mouseIsOverTooltip = true);
this._tooltip.addEventListener('mouseleave', () => this._mouseIsOverTooltip = false);
}
_show() {
this.hidden = false;
this._mouseIsOverTarget = true;
}
_hide() {
this._mouseIsOverTarget = false;
// If the mouse is not over the tooltip, hide it
if (!this._mouseIsOverTooltip) {
this.hidden = true;
}
}
}
window.customElements.define("knowlo-tooltip", KnowloTooltip);
In the code above, _mouseIsOverTarget
and _mouseIsOverTooltip
are flags that are used to track whether the mouse is currently over the target or the tooltip. _show
and _hide
are adjusted to set these flags accordingly. The mouseenter
and mouseleave
events of the tooltip are also listened to and used to adjust the _mouseIsOverTooltip
flag.
This way, when the mouse leaves the target, _hide
will be called, but the tooltip will only actually be hidden if the mouse is not currently over the tooltip.
This suggestion didn’t work but it gave me enough direction to solve the issue. After fixing this issue, I spent some time pulling the data from the KnowloAPI, authenticating anonymous users using Cognito Identity Pool, allowing users to ask questions, and fixing styles. The final code would be too long for this article, but I might explain the important parts of it in one of the next articles.
Testing Web Components
Slobodan: Hey CofounderGPT, what is the best way to test this Web Component? I would love to have both unit and integration tests for it.
CofounderGPT: Testing web components involves both unit tests and integration tests to ensure they work correctly in isolation and when interacting with other components or services.
1. Unit Testing:
You would typically use a JavaScript testing framework like Jest or Mocha for this. The purpose of unit tests is to verify the behavior of the component in isolation. For instance, you might test that a tooltip displays the correct text when given a certain input, or that it fetches data correctly when given a certain tooltip ID.
Unit testing a web component can be challenging because they often interact with the DOM and may rely on Shadow DOM isolation. You can use a library like @open-wc/testing
which provides a set of helpers to allow testing of web components in a node environment.
2. Integration Testing:
Integration tests ensure that your component works correctly when interacting with other components or services. You could also use Jest or Mocha for this, along with a tool that can simulate a browser environment like Puppeteer or Cypress.
The integration tests for your tooltip component could include tests that:
- Check that the tooltip appears when the correct attribute is added to an element.
- Check that the correct tooltip is fetched from the API based on the tooltip ID.
- Check that the tooltip displays correctly within the context of a larger application.
3. End-to-End Testing:
Beyond unit and integration tests, you might also want to consider end-to-end tests. These tests simulate user behaviors and verify that everything works correctly from a user’s perspective. Tools like Cypress or Playwright are excellent for writing end-to-end tests.
For example, an end-to-end test for your tooltip might simulate a user hovering over a tooltip, clicking on it to ask a question, and then verify that the correct information is displayed.
4. Visual Regression Testing:
Given the visual nature of a tooltip component, it’s also a good idea to include some kind of visual regression testing in your test suite. Visual regression testing tools take screenshots of your component in different states and then compare these screenshots to reference images to check for any visual differences. Tools like Percy, Applitools, or even Jest’s screenshot matcher can help with this.
Lastly, make sure to follow best practices for testability in your code. Use clear, consistent selectors for your elements, and make sure every part of your component’s public API is testable. Remember that testing is not just about catching bugs, but also about creating a robust, maintainable codebase.
Slobodan: Thanks! Can you show me some examples?
CofounderGPT: Sure, I’ll provide a simple example for each kind of test using Jest and Puppeteer. Please note that these examples are very basic and actual tests might be more complex based on your component’s functionality.
Unit Test
To unit test your Web Component, you could use Jest along with @testing-library/dom
or @open-wc/testing
. Here’s a basic example:
import { html, fixture, expect } from '@open-wc/testing';
it('displays tooltip content correctly', async () => {
const element = await fixture(html`
<knowlo-tooltip data-knowlo-tooltip-id="123"></knowlo-tooltip>
`);
// Simulate the response from the API
element._tooltipContent = 'Test content';
element.render();
const tooltipElem = element.shadowRoot.querySelector('div');
expect(tooltipElem.textContent).to.equal('Test content');
});
In this test, we’re creating a knowlo-tooltip
element, setting the tooltip content, and then verifying that the tooltip displays the content correctly.
Integration Test
An integration test might involve testing how the KnowloTooltip
interacts with the API. Here’s an example using Jest and nock
:
import { html, fixture, expect } from '@open-wc/testing';
import nock from 'nock';
it('fetches tooltip from the API correctly', async () => {
// Intercept the API request and respond with a test tooltip
nock('https://api.knowlo.io')
.get('/tooltips/123')
.reply(200, { content: 'Test content' });
const element = await fixture(html`
<knowlo-tooltip data-knowlo-tooltip-id="123"></knowlo-tooltip>
`);
// Wait for the component to fetch the tooltip and update
await new Promise((r) => setTimeout(r, 1000));
const tooltipElem = element.shadowRoot.querySelector('div');
expect(tooltipElem.textContent).to.equal('Test content');
});
In this test, we’re intercepting the HTTP request that the component makes to the API and responding with a test tooltip.
End-to-End Test
End-to-end tests are typically done with a tool like Puppeteer or Cypress that can simulate a real browser. Here’s an example using Puppeteer:
import puppeteer from 'puppeteer';
it('displays tooltip when hovering over an element', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000'); // replace with your actual url
// Hover over the element that has the tooltip
await page.hover('#element-with-tooltip');
// Wait for the tooltip to appear
await page.waitForSelector('.tooltip');
// Check that the tooltip is displayed with correct content
const tooltipContent = await page.$eval('.tooltip', el => el.textContent);
expect(tooltipContent).toBe('Test content');
await browser.close();
});
In this test, we’re using Puppeteer to simulate a user hovering over an element with a tooltip and then verifying that the tooltip appears with the correct content.
Visual Regression Test
Visual regression tests usually involve taking screenshots and comparing them to a baseline. Here’s an example using Puppeteer and Jest:
import puppeteer from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
it('renders tooltip correctly', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000'); // replace with your actual url
// Hover over the element that has the tooltip
await page.hover('#element-with-tooltip');
// Wait for the tooltip to appear
await page.waitForSelector('.tooltip');
// Take a screenshot
const screenshot = await page.screenshot();
// Compare the screenshot to the baseline
expect(screenshot).toMatchImageSnapshot();
await browser.close();
});
In this test, we’re taking a screenshot of the page after hovering over an element with a tooltip and comparing it to a baseline image to check for any visual differences.
Please note, you should modify these examples based on the actual functionality of your Web Component.
While my approach is to do as little as possible while working on the MVP for Knowlo, I think it’s worth spending some time on the tooltips and writing proper tests. Mainly because users will add them to their websites and web apps, and we do not want to break anything there. It’s ok if our administrator dashboard is not perfect for the MVP, but we need to spend a bit more time on the part of the code that will be embedded.
Scoreboard
Time spent today: 8h
Total time spent: 186h
Investment today: $0 USD
Total investment: $1,284.45 USD
Beta list subscribers: 89
Paying customers: 0
Revenue: $0
What’s next?
My next step is a short vacation and a cocktail on a beach. CofounderGPT will help Lav do some other interesting and important things. After I get back, CofounderGPT will help me to write proper tests for our new Web Component and finalize the other remaining things on the dashboard. We are getting closer to the MVP.
Comments are closed