Attribute toggling with LiveView JS

Why?

With Tailwind, you can style your components based on data-* attribute’s value (read more).

How to update DOM attribute with LiveView.JS? You can use JS.set_attribute({“data-state”, “open”}) for example. But the problem is your code doesn’t know current data-state value, and there is no way to do that using LiveView.JS.

Using JS.dispatch

Fortunately, Phoenix.LiveView.JS support dispatch/2 which triggers a custom event on self or target specified by the to: "selector" option. I came up with the following solution:

A custom event mrk:toggle-data, which toggles the attribute on target based on a data-toggle attribute.

data-toggle accepts a string pattern like data-toggle="dataset_key|on_value|off_value"

  • dataset_key: is the x part of the data-x attribute selector
  • on_value: is the value you want to have when the attribute is toggled to on state
  • off_value: is the value you want to have when the attribute is toggled to off state

for example:

<section id="section_id" class="group" data-toggle="open|true|false">
  <div class="hidden group-data-[open='true']:block">
    I will be hidden until the `section.group` element doesn't have a
    data-open="true" attribute
  </div>
</section>

You could trigger the toggle event like so:

<button phx-click={JS.dispatch("mrk:toggle-data", to: "#section_id")}>
    show section content
</button>

if you want some value to be default you can just add that attribute yourself:

<section id="section_id" data-open="false" ...truncated>...truncated</section>

Implementation

Add this to your app.js:

const toggleData = (e: Event) => {
 const target = e.target;
 const toggleData = target.getAttribute("data-toggle");

 const [key, on, off] = toggleData.split("|");

 if (target.dataset[key] === on) {
  target.dataset[key] = off;
 } else {
  target.dataset[key] = on;
 }
};

window.addEventListener("mrk:toggle-data", toggleData);

or if you want a typescript version:

const safeTarget = (event: Event): HTMLElement => {
  const target = event.target
  if (!(target instanceof HTMLElement))
    throw new Error('Event target is not an HTMLElement')
  return target
}

const safeAttribute = <T = string>(el: HTMLElement, attribute: string): T => {
  const value = el.getAttribute(attribute)
  if (!value) {
    throw new Error(`Attribute ${attribute} not found`)
  }
  return value as unknown as T
}

const toggleData = (e: Event) => {
  const target = safeTarget(e)
  const toggleData = safeAttribute(target, 'data-toggle')

  const [key, on, off] = toggleData.split('|')

  if (target.dataset[key] === on) {
    target.dataset[key] = off
  } else {
    target.dataset[key] = on
  }
}

window.addEventListener('mrk:toggle-data', toggleData)

Conclusion

With JS.dispatch you can easily extend javascript’s functionality for LiveView. You can also create helpers functions like:

MarkoJS.toggle_attribute("#section_id")

Thanks for reading.