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 thex
part of thedata-x
attribute selectoron_value
: is the value you want to have when the attribute is toggled toon
stateoff_value
: is the value you want to have when the attribute is toggled tooff
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.