Tooltips in Phoenix LiveView
There are a few options to integrate tooltip functionality into Phoenix LiveView. This article covers integrating a Phoenix LiveView with one popular library tippy.js, ensuring any updates from LiveView are reflected in tooltip state.
Background
tippy.js is a powerful, versatile and lightweight Javascript tooltip library. It provides the logic (and optional styling) of elements that “pop out” and float next to a target element. Although the last version was released three years ago, it receives over a million downloads a week. The approach here can be transferred to other libraries like Floating UI.
With Phoenix LiveView applications, we may wish to have the content of tooltips (and their styling) vary depending on the current application state.
Because tippy.js takes care of configuring and rendering the tooltip, and stores internal state outside of the element, we’ll need to use a LiveView client hook to ensure updates to the element are reflected in the tippy.js internal state associated with the element.
(Need a refresher on how to integrate client-side libraries that take care of rendering? See this previous article: Phoenix LiveView, hooks, and push-event: json-view)
Installation
In a Phoenix LiveView application, let's install the library:
1npm install --prefix assets tippy.js
We’ll need to now include the Javascript and (default) CSS in separate ways.
For the Javascript code - We can then include tippy.js
in assets/js/app.js
, with the hook to come:
1import tippy from "tippy.js"
And the CSS can be imported in assets/css/app.css
:
1@import "tippy.js/dist/tippy.css";
Configuring tooltips (tippy.js)
With this library now included in our application, we can now invoke tippy
, a function that creates tooltip context associated with a particular element. We can use the returned value (an instance
) to control, update and invoke the tooltip as necessary.
tippy.js includes a number of configurable properties (with associated defaults) - the documentation covers all of these.
We can keep the need for custom Javascript code very light, and integrate with LiveView well, by using data-tippy
attributes. We can access these properties via the dataset
attribute, which we’ll do so in the hook in the following section.
When creating the tooltip instance, the tippy
function actually reads these data-tippy
elements. However, if we update those attribute values with LiveView, it won’t update the tooltip. This is a limitation of the library, documented in the FAQ.
So, keeping LiveView’s update mechanism in mind, we’ll have to write a hook to create the tooltip instance when the element is mounted, and update the properties correctly when the element is updated.
Let’s take a look at the element first, to see what kind of state we’ll need to deal with.
The HTML element
Here’s a button
element that will integrate with the hook we’re about to write:
1<button id="example-button" phx-hook="Tippy" data-tippy-content="Hello!" data-tippy-placement="bottom">2 Hover over me3</button>
There are several things to note here:
id
is required for all elements that have aphx-hook
associated with them, so Phoenix LiveView can associate internal state by element IDphx-hook
associates a particular hook. Only (at most) one hook is permitted per elementdata-tippy-
is the prefix for each attribute. We’ll need to process thesedataset
elements (see the dataset property documentation) to pass a configuration object when we come to update the properties associated with the tooltip. In short:data-tippy-placement=”bottom”
in the DOM becomesthis.el.dataset.tippyPlacement // => “bottom”
in Javascript.
The LiveView hook
Here’s the code for the hook. Let’s go through the below section by section.
1const Hooks = { 2 Tippy: { 3 mounted() { 4 this.instance = tippy(this.el); 5 }, 6 updated() { 7 this.instance.setProps( 8 Object.fromEntries( 9 Object.entries(this.el.dataset)10 .filter(([k]) => k.startsWith("tippy"))11 .map(([k, v]) => [12 this.tippyPropName(k),13 v14 ])15 )16 );17 },18 destroyed() {19 this.instance.destroy();20 },21 22 tippyPropName(k) {23 const strippedName = k.replace("tippy", "");24 return strippedName.charAt(0).toLowerCase() + strippedName.slice(1);25 },26 },27};
mounted()
When the element is first added to the page, the mounted()
callback is invoked. We’ll need to create the tooltip instance associated with the DOM element (this.el
):
1this.instance = tippy(this.el);
After this, tippy.js takes care of attaching to the relevant DOM events and invoking the logic to display the tooltip.
updated()
As noted above, tippy.js doesn’t update its internal state if the values of attributes change.
1this.instance.setProps( 2 Object.fromEntries( 3 Object.entries(this.el.dataset) 4 .filter(([k]) => k.startsWith("tippy")) 5 .map(([k, v]) => [ 6 this.tippyPropName(k), 7 v 8 ]) 9 )10);11
Step by step:
It starts with
this.el.dataset
, which contains all data attributes of an HTML element.It filters these attributes to only include those that start with "tippy" (
data-tippy
in the HTML)For each filtered attribute, it transforms the attribute name into a valid Tippy.js property name using a tippyPropName method.
It creates an object from these transformed key-value pairs.
Finally, it sets these properties on the Tippy.js instance using setProps().
Another alternative: we could invoke tippy
again to create another tooltip. But this has the clearest purpose and intention, even if the logic to transform attribute names isn’t necessarily easy to parse.
destroyed()
Without this, tippy.js tooltip instances accumulate during LiveView updates, causing memory leaks.
tippyPropName(k)
We need to remove the tippy
prefix from the property name, and convert the casing of the remaining string (from a leading uppercase character to a lower one). So tippyShowOnCreate
becomes showOnCreate
, the object key that the setProps
method expects.
Alternatives
tippy.js
is by no means the only tooltip library that can be integrated with Phoenix LiveView:
alpine-tooltip is a popular plugin for Alpine.js, commonly used as a simple Javascript dynamic layer in Phoenix LiveView applications
Floating UI is a library (superseding popper.js) that helps you create “floating” elements such as tooltips, popovers, dropdowns and more. Floating UI is framewor
Use Phoenix LiveView component libraries like salad_ui or Chelekom, which contain their own tooltip component implementations.
Conclusion
Integrating tippy.js with Phoenix LiveView provides a powerful and flexible solution for implementing tooltips in your application. By utilizing a LiveView client hook, we can ensure that any updates to the tooltip content or styling are seamlessly reflected in the tippy.js internal state.
This approach allows for dynamic, state-dependent tooltips that respond to changes in the application's context, enhancing the user experience and interactivity of your Phoenix LiveView application.
While tippy.js is a popular choice, it's worth noting that there are alternative libraries and approaches available for implementing tooltips in Phoenix LiveView. Whether you choose tippy.js, Alpine.js plugins, Floating UI, or Phoenix-specific component libraries, the key is to find a solution that best fits your project's needs and integrates smoothly with LiveView's update mechanism.