Implement `toggle_attribute` with LiveView JS
Tags: phoenix, liveview, elixir, webdev
Published: 2023-07-13 06:59:00

## Why?

I'm working on rebuilding interface for my project [OrangeCMS](https://github.com/bluzky/orangecms) using TailwindCSS framework. With Tailwind, you can style your coponents based on `data-*` attribute's value ([read more](https://tailwindcss.com/docs/hover-focus-and-other-states#data-attributes)).

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:

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


## Implementation

**HEEX markup**

```html
<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>
```

**Update app.js**

append this to end of your `app.js`
```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";
    }
  });

```

This is the result

![Demo result for a simple dropdown](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dnn6bc6zc408fzp1pcqc.gif)

## 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](https://github.com/bluzky/orangecms)

```elixir
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")
```

Thanks for reading. Your feedback are welcome.