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 casetoggle_attribute
-
attr
: attribute name, in this casedata-state
-
values
: 2 values fordata-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"]}])
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>
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";
}
});
This is the result
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")
Thanks for reading. Your feedback are welcome.
Top comments (3)
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:
where
data-toggle has a pattern string
open
thex
part of thedata-x
true|false
the values to toggle between.and here's the event listener:
I mimic
JS.toggle
fromLiveView.JS
:D, I guess they don't want to add too many extra html attributesYeah completely valid :)