DEV Community

Cover image for Implement `toggle_attribute` with LiveView JS
Dung Nguyen
Dung Nguyen

Posted on

Implement `toggle_attribute` with LiveView JS

Why?

I'm working on rebuilding interface for my project OrangeCMS using TailwindCSS framework. With Tailwind, you can style your coponents 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 allow trigger custom event and pass data via event.detail attribute. So my idea is:

  • Implement a custom event lad:exec with following details: [action, {attr: <attribute_name>, values: <list of values> }]

    • action: action to execute. in this case toggle_attribute
    • attr: attribute name, in this case data-state
    • values: 2 values for data-state, which I want to toggle between them

It would be something like this:

JS.dispatch("lad:exec", to: "#some-id", detail: ["toggle_attribute", %{attr: "data-state", values: ["open", "closed"]}])
Enter fullscreen mode Exit fullscreen mode

Implementation

HEEX markup

<div class="dropdown group" id="some-id" data-state="closed">
    <button 
        class="trigger-btn"
        phx-click={JS.dispatch("lad:exec", to: "#some-id", detail: ["toggle_attribute", %{attr: "data-state", values: ["open", "closed"]}])}
    >Click me</button>
    <div class="dropdown-content hidden group-data-[state=open]:block">
        <ul>
            <li> item 1</li>
            <li> item 2</li>
            <li> item 3</li>
        </ul>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Update app.js

append this to end of your app.js

const commands = {
    trigger_attribute(target, {attr, values}){
        if (!values || values.length != 2)
            throw "values must be an array of length 2";

        // get current attribute's value
        const current = target.getAttribute(attr);

        // get the other value and update it
        const nextValue = values.find((v) => v != current);
        target.setAttribute(attr, nextValue);
    }
}

// listen to dispatched event
window.addEventListener("lad:exec", (event) => {
    if (Array.isArray(event.detail)) {
      const [command, args] = event.detail
      commands[command](event.target, args)
    } else {
      throw "lad:exec expected command's arguments as an array";
    }
  });

Enter fullscreen mode Exit fullscreen mode

This is the result

Demo result for a simple dropdown

Conclusion

With JS.dispatch you can easily extend javascript's functionality for LiveView. You can even implement some helper functions like what I implemented for OrangeCMS

JS.add_class("bg-green", to: "#my-id")
|> LadJS.toggle_attribute({"data-state", ~w(open closed)}, to: ".my-container")
|> LadJS.toggle_class("hidden", to: "#my-id")
Enter fullscreen mode Exit fullscreen mode

Thanks for reading. Your feedback are welcome.

Top comments (3)

Collapse
 
neophen profile image
Mykolas Mankevicius

I've actually done this as well but i had a slightly different approach instead of the toggle event having to know everything, all it needs is the id, and the target knows everything else it needs to by himself, decoupling the event caller from the having to know anything but the id of the component:
You call it like so:
phx-click={JS.dispatch("mrk:toggle-data", to: "##{@content.id}")}

The target components have these attributes:

<section id={@content.id} data-open="false" data-toggle="open|true|false">
Enter fullscreen mode Exit fullscreen mode

where
data-toggle has a pattern string open the x part of the data-x true|false the values to toggle between.

and here's the event listener:

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

export 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
}

window.addEventListener('mrk:toggle-data', (e: Event) => {
  const element = safeTarget(e)
  const toggleData = safeAttribute(element, 'data-toggle')

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

  if (element.dataset[key] === on) {
    element.dataset[key] = off
  } else {
    element.dataset[key] = on
  }
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bluzky profile image
Dung Nguyen

I mimic JS.toggle from LiveView.JS :D, I guess they don't want to add too many extra html attributes

Collapse
 
neophen profile image
Mykolas Mankevicius

Yeah completely valid :)