nsalva

nsalva

Provide reliable way to avoid LV to erase DOM Attributes set by JS

Hello,

i would like to expose a issue i am currently facing , and it my mind can have a solution as Quality of Life on using Phoenix together with javascript ecosystem.

Preface

Some UI integration doesn’t need to be kept on the Server itself, causing just latency. I don’t wanna that to open a dropdown the end user should wait this:

[ Click ] -> [ Send Event to Server ] -> [ Compute new DOM ]
-> [ Send DIFF to the browser ]

It’s very expensive computation to just deal with a dropdown opening…

Issue

To eliminate the need for server round-trips, and keeping state on the server state.
I can use component’s Hooks API to deal “no-critical-ui” interaction with the UI.

However, this approach sometimes conflicts with Phoenix’s synchronization and diffing mechanism for rendering live components. The most common case i can recall is the usage of <dialog /> HTML tag which isn’t kept open across rerendering.

I prepare a Draft MVP with a very rushy patch which implement my solution, and a repo containing the “issue” and relative patch with the PR phoenix version.

PR: fix: Draft Proposal API ignoreDOMPatchAttributes by ssalv · Pull Request #3574 · phoenixframework/phoenix_live_view · GitHub
Issue: issue-liveview-deps-patchDOM/main_bugged.exs at main · ssalv/issue-liveview-deps-patchDOM · GitHub
Fixed with PR’s API: issue-liveview-deps-patchDOM/main_patched_example.exs at main · ssalv/issue-liveview-deps-patchDOM · GitHub

    const myButtonHook = {
        mounted() {
          // This is the important part
          this.ignoreDOMPatchAttributes(["data-times-clicked"]);
          // End Important Part
          this.el.dataset.timesClicked = 0;
          this.el.addEventListener("click", e => {
            this.el.dataset.timesClicked = (parseInt(this.el.dataset.timesClicked) || 0) + 1;
          });
        },
        updated() {
          console.log("Button updated", this.el.dataset.timesClicked); // Now this works
        },
    };
Real World Example

I have my Normal component, which renders a dropdown, the dropdown state is kept using data-state="open|close" from the JS hook, and it’s placed inside a live component.

This results in every update ( handled by example handle_event ) on the triggering an update to the live component, which resets my data state to the component’s default state.

The solution seems simple, doesn’t it?

Already exists some workaround to this issue:

phx-update=“ignore”

While that might work, it make the target’s HTMLNode children not able of adapting to changes.

Dealing inside the update hook function

This can be good solution until the changes is quite simple, like the example, but in case of complex solutions ( Image a Javascript library which animate the element using the style attribute) is not valid solution anymore…

Patching app.js

I could apply a workaround by implementing a custom patcher, something like this in the app.js in phoenix

dom: {
    onBeforeElUpdated(fromEl, toEl) {
      const isPopover = fromEl.getAttribute("phx-hook") == "Popover";
      if (isPopover) {
        for (const attr of fromEl.attributes) {
          if (attr.name == "data-open") {
            toEl.setAttribute(attr.name, attr.value);
          }
        }
      }
    },
},
Notify back the server about the change

In my case, this could also serve as a potential solution, but it necessitates storing the dropdown state in the server session (live_component) an unnecessary detail for the LiveView itself ( dropdown open or close ) and a waste of resources, this is how actually most of the components in the ecosystem works from what i saw in my experience.

Real Issue behind all these solutions

Although these solutions might work individually, they fall short when my component is offered as a part of an library. I didn’t found a reliable way to expose them or ensure they work out of the box, for instance, from my hook.

This issue often prove unreliable for interactivity and animation libraries, which may rely on the style attribute of a node, animations are reset or broken due to LiveView updates, creating conflicts with the library’s functionality.

Something which is part of the Phoenix API is hardcoded patched for this issue, for example: assets/js/phoenix_live_view/dom.js#L318

Possible solution or proposal

It would be amazing have something similar to the this.handleEvent, capable of specify how the component is patched during re-rendering with MorphJS.

In my mind, something like this could provide a highly effective tool for addressing this issue, while preserving the advantages of live components

// My Dropdown UI Interactivity Hook
export default {
   mounted() {
	   this.handleDOMPatch((from, to) => {
			for (const attr of fromEl.attributes) {
		        if (attr.name.startsWith("data-open")) {
		            toEl.setAttribute(attr.name, attr.value);
		        }
	        }
	   });
	   // Or Simpler version
	   
   }
   ...
}

-another example-

// My Animated Component
const fancyJavascriptAnimationLibrary = require("fancy-js");

export default {
	mounted() {
	   this.handleDOMPatch((from, to) => {
			for (const attr of fromEl.attributes) {
		        if (attr.name == "style"){
			        // Keep Style consistent across the updates
		            toEl.setAttribute(attr.name, attr.value);
		        }
	        }
	   });
	   fancyJavascriptAnimationLibrary.animate({scale: {to: 1, from: 0}});
   }
   ...
}

This function should be eval exclusively for this specific DOM node identified by its ID (hooks require them so not a issue), similar to the this.handleEvent API.

-or- I did a fast patch on LV Code to supports this

Additionally, a declarative approach works, aligning more closely with the others API Phoenix.

<button
	id="button-with-animation"
	...
	phx-ignore-attrs="style,data-animation-*"
>
	Fancy Button
</button>

and in the DOM Patcher we keep the attributes which match with ignoreAttrs dataset.

Any thoughts? Is there a reliable possible solution that may already be implemented, one that I might have missed it?

Thanks for reading

Most Liked

steffend

steffend

Phoenix Core Team

Hey @nsalva,

sorry for the late reply!

That’s what JS commands are for. They provide a way to perform “sticky” operations that are always re-applied to DOM elements. Since LV 1.0.0-rc.7, those are also available to Hooks:

const myButtonHook = {
  mounted() {
    const that = this;
    this.js().setAttribute(this.el, "data-times-clicked", 0);
    this.el.addEventListener("click", e => {
      this.js().setAttribute(this.el, "data-times-clicked", (parseInt(that.el.dataset.timesClicked) || 0) + 1);
    });
  },
  updated() {
    console.log("Button updated", this.el.dataset.timesClicked);
  },
};

For more details, see: phoenix_live_view/assets/js/phoenix_live_view/view_hook.js at c44c48e09c3705fd9510d491d77f5237929be08e · phoenixframework/phoenix_live_view · GitHub

Where Next?

Popular in Proposals: Ideas Top

sbennett33
When building a component library, it is often useful to give users the ability to customize the underlying element or component to use. ...
New
markevans
Hi! I feel like Phoenix is slightly missing a trick when it comes to front-end Javascript libraries like React, Svelte, etc. I feel tha...
New
woylie
We are seeing a lot of warning logs like this: navigate event to "https://someurl" failed because you are redirecting across live_sessio...
New
mikesax
On a Rails/Turbo site, the first page is typically loaded using http GET and then sockets are used navigate and replace HTML content for ...
New
cevado
IEx is a very powerfull shell and it would be awesome to have all this power integrated inside a code editor. Clojure enables something l...
New
bartblast
This could resolve to {[a: 1, b: 2]}. Was it ever considered to allow such syntax? Notice this: {:abc, a: 1, b: 2} and this: my_fun(:abc,...
New
kccarter
This is likely a feature request unless we’re overlooking something, but it would be a nice improvement to the developer experience if th...
New
Oliver
One common problem we face in constructing lists is that there is (AFAIK) no support for conditionally inserting members into list declar...
New
dibok
Hi, I’m trying to use phoenix.js in my Qt QML project which has it’s own buildin JavaScript engine. Problem is that (what I googled so f...
New
eagle-head
Hi everyone, I’ve been researching Content Security Policy Level 3 support in Phoenix and wanted to share my findings and a proposal for...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement