phoenixframework / phoenix

Peace of mind from prototype to production
https://www.phoenixframework.org
MIT License
21.35k stars 2.87k forks source link

AlpineJS event handlers break live view rendering #4151

Closed cschmatzler closed 3 years ago

cschmatzler commented 3 years ago

Environment

Hi. So the title is kinda vague because the example that I cooked up actually shows multiple issues. I love AlpineJS and Chris himself said here that it

plays nicely with LiveView’s own DOM patching [...]

I have unfortunately found the opposite to be true while exploring Phoenix. My example repo is here: https://github.com/cschmatzler/phoenix-alpine-example

This is what mix phx.new with --live argument creates, with the following changes:

  1. The database password in config/dev.exs has been changed to dev to work in my environment. You might want to change it back when cloning.
  2. Alpine ^2.8.0 has been added to assets/packages.json.
  3. assets/js/app.js has been updated to include the liveSocket preferences Chris mentioned in the post I linked above. This has been copy and pasted.
  4. lib/minimal_alpine_rendering_example_web/page_live.ex and .html.leex have been changed to showcase my issue.

Alpine x-on:click ("This uses x-on:click")

Expected behavior

Our example allows us to add an item to a list of maps. In my actual application, it is a list of structs that Ecto fetches from the database, but as far as I know structs are just maps under the hood and our example with a normal map works just the same. With the two elements initially added to the list in mount/3, both buttons "Alert with element" open an alert with the correct corresponding list item. The button for Element 1 produces "1, Element 1", the button for Element 2 produces "2, Element 2". When we add an element to the list by clicking "Add an element", Element 3 shows up. It is prepended to the list in handle_event/3 inside page_live.ex. Now, clicking our "Alert with element" button should produce "3, Element 3" for that table row.

Actual behavior

It does not. Actually, our buttons now map as follows:

Let's add a fourth element. The buttons now are:

Weirdly enough, inspecting the last button reveals <button x-data x-on:click="alert('4, Element 4')" :key="4" key="4">Alert with element</button>... just pressing it alerts with a different content. I have not been able to figure out where the discrepancy between markup and actual JavaScript action comes from, but it does work without LiveView.

Alpine @click ("This uses @click")

Expected behavior

Alpine allows using @click as shortcut declaration for x-on:click.

Actual behavior

Using this shortcut breaks LiveView completely. phoenix_live_view.js throws the Uncaught DOMException: Failed to execute 'setAttribute' on 'Element': '@click' is not a valid attribute name. in the console. Interestingly, adding an element replaces the previous table row. The first one always remains, but the second is replaced by the newly added element. As such, we can never have more than two elements rendered in the table (unless we start with more than two in mount/3, in which case there will always be as many table rows rendered as the length of the initial list, with the topmost one being replaced by each new one).

Additionally, after @click, changes are not rendered at all anymore, as seen after the "This is without any alpine declarations, but after @click is used" heading.

Closing

The @click behavior is not extremely serious because it is just a shortcut, but x-on:click breaking when looping through a list actually breaks my page. I hope my example is enough to reproduce, if there are any questions I am happy to help out.

cschmatzler commented 3 years ago

For x-on:click I have added a workaround using x-data, with which Alpine DOM tracking does not fail anymore. That way, the first issue seems resolved and might not need further evaluation.

@click rendering is still broken and I have not found a solution.

cschmatzler commented 3 years ago

So the HTML5 spec says the following about tag attributes:

Attribute names must consist of one or more characters other than the space characters, U+0000 NULL, U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), U+003E GREATER-THAN SIGN (>), U+002F SOLIDUS (/), and U+003D EQUALS SIGN (=) characters, the control characters, and any characters that are not defined by Unicode.

This, to me, means an @ should be a valid attribute. On the other hand, elem.setAttribute('@click', 'value') fails with the error in my initial issue. Since this doesn't seem to be a Phoenix issue but a limitation of setAttribute, you guys might want to close this as out of scope.

chrismccord commented 3 years ago

You need to provide an ID on the x-data containers:

<button id="<%= element.id %>" x-data x-on:click="alert('<%= element.id %>, <%= element.name %>')" :key="<%= element.id %>">Alert with element</button>

When morphdom patches the DOM and unkeyed children are shuffled, they are destroyed and recreated. Adding an ID to the node ensures it exists across the patch. Thanks!