-
Notifications
You must be signed in to change notification settings - Fork 946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
phx-hook does not run in elements dynamically added to the DOM #2563
Comments
I don’t think this can work in practice without additional code. phx-click works because we can listen in all clicks. Listening to all page change events would be expensive. We could maybe provide an API to mount hooks that have not been mounted yet, and you would call it when necessary. |
Hi @josevalim! Thanks for the quick reply! I think that having a way to force-mount the hooks manually would already allow us to achieve what we need. We could "activate" it manually here if we have this special API and I'd hope everything works as expected: // Clones the template that we want to "teleport" somewhere else
this.clone = this.el.content.cloneNode(true).firstElementChild;
// Inserts the clone into the DOM here (after the body for instance)
// "Activate" the element and makes bindings work
superDuperPhoenixMethodThatMakesThisWork(this.clone) I just want to be sure that if we do this, all bindings would work as expected; like the element was available from the page load. Otherwise, we would have to resort to a third-party library like Alpine; losing many advantages of using just LiveView constructs. Besides making the configuration a little more convoluted for users, I think the user experience wouldn't be the same overall. Update: Another alternative could be adding a |
Feel free to attempt a PR to explicitly mount hooks. |
Cool @josevalim! Do you have an idea of how this API would work? We are not that familiar with the LiveView code base, but it would be a cool exercise nonetheless. PS.: I appreciate if you have any pointers for us as well. Cheers!👍 |
What are you doing within your hook? I need to hear more about the use case because opening this up will necessarily be fragile if we change the way lifecycle works for hooks on the client. For example, if you're already using js to build DOM containers and whatnot, you can wire up your custom js listeners/logic in the same place without the hook, no? |
Hey @chrismccord! We have a Portal implementation that uses a template tag to hold its contents: https://github.com/thiagomajesk/swish/blob/master/assets/hooks/portal.js. If you are not familiar with what a Portal is: It's simply a way to render elements outside of their parent tree, making rendering overlayed items like dialogs and modals a lot simpler (because of CSS constraints). Feel free to browse the project, but the gist of the problem is this: We are currently implementing a toast element that you can see here: thiagomajesk/swish#3. A Toast also needs to have a hook to hold some state for closing it (see previous discussion here: #2550). Since the contents of the template are not rendered in the DOM yet; by the time LiveView runs its logic, it does not register the hooks. So, when the contents of the Portal (template) are placed in the body, the hooks that exist in that Node's tree are not mounted. This is an example of what is going to be rendered into the HTML: <!-- This hook controls where to place the portal,
how to open and how to dispose of the portal after it's closed.
-->
<template phx-hook="Swish.Portal">
<!-- This hook controls how long the toast remains open,
listens for user interaction, and delays the closing of the toast.
-->
<div phx-hook="Swish.Toast" phx-mounted={show(@toast) |> hide(@toast)}>
Hello World! I'm a very cool toast
</div>
</template> So, the content of a Portal is only part of the DOM when the Portal is "opened". For Toasts, this happens after a few milliseconds after the page loads and for Modals, this happens after a user interacts with a trigger. We currently have no way to make LiveView register and exec the hooks/ bindings from those elements. |
In theory one could define custom callbacks for the toast component and make the portal component try to send maybe a So it would be nice to at least be able to run the hooks manually, otherwise even if we manually implemented our library's functionality using some "homemade hooks", the behavior would be wrong for the library users. |
Hey @chrismccord, I managed to get this behavior of manually mounting elements and running hooks by using the following piece of code in our portal component: handleOpen() {
this.clone = this.el.content.cloneNode(true).firstElementChild;
// Opens the portal and teleports clone to target.
// Await a little before opening so animation ca be properly displayed.
updates[this.update](this.target, this.clone);
// forcefully run hooks (very similar to the execNewMounted function)
this.clone.querySelectorAll("[phx-hook]").forEach(el => {
window.liveSocket.main.maybeAddNewHook(el)
})
this.clone.querySelectorAll("[phx-mounted]").forEach(el => {
window.liveSocket.main.maybeMounted(el)
})
// Await until next tick to register the forwarded events
forwardEvents(this.el, this.clone)
}, Everything is working as expected, but this approach does not feel solid, it would be much better if we had a stable and documented function/API that one could use without fearing for breaking changes. Do you think something like this would be possible? I think that no component lifecycle changes are required in order to achieve this. Also, it would be much better if we could stop depending on the LiveSocket object, but I'm not sure if that would be possible. One possible implementation would be making the execNewMounted function receive the element as a parameter instead of referencing |
Hi @chrismccord, @josevalim! Sorry to bother... Do you think we have a course of action for this issue? We stopped developing the library for the time being until it's clear how we can properly tackle the problem. Like @viniciusmuller pointed out, we managed to test the solution by using this workaround and it kinda works. However, we are currently blocked by using this unofficial API. Hope we can get more information so we can proceed. Cheers! |
Hey @josevalim @chrismccord, are there any updates on this? We would like your feedback to continue planning an API for and validating this issue's idea. |
It's going to take a while for me to get to this one as I have several other things to get through on the backburner. In general I am hesitant to support this since by definition it's for client-controlled DOM, yet we want to attach server generated things to it, which could be brittle based on this assumption not being used anywhere else today. Once I have some time to review this I will ping the issue/PR. Thanks! |
I'm inclined to wait on this one because it's an interface that's likely to tie us down in the future. I'm not yet convinced that client controlled DOM should be commingled inside the LiveView DOM patch lifecycle like this, so it would be best to solve things on your JS side. Thanks! |
Hi @chrismccord! Do you have any suggestions on achieving this on the "JS side"? As far as I remember, there's still the limitation that we can't mount the hooks after the first load - which renders the usage of hooks pointless for this use-case. So, for the time being, this means we won't be able separate concerns for components whose logic resides in a hook because there's no way to mount them "on demand". PS.: I think that activating hooks like this could become a fairly common use case now that we have streams - like activating content (eg.: a column counter) added dynamically (eg: row added to a table). |
hooks are mounted any time their element first appears in the DOM, so this won't be a problem for streams or anything that is added after the initial load like live navigated elements, etc. |
Now I'm confused by what you mean 🤔... I was under the impression that this was the problem we were facing. We add a new element to the DOM, but the hook is not mounted - do streams handle this differently? I wonder if I haven't explained the problem correctly then, imagine this:
Perhaps I'm misinterpreting what you mean by "hooks are mounted any time their element first appears in the DOM"? What am I missing here that is different from the way you described streams work? (Just to be sure we are talking about the same thing). |
Every time the server sends something, if there are new hooks, they should be mounted. There is nothing special for streams to make it work. Afaik It is part of the morphdom on traversal. |
The DOM nodes that liveview adds are mounted properly. We do not use mutation observer or anything to track arbitrary changes to the DOM itself. Opening up what you are requesting is a big maintenance burden and something that is likely to change in the future if we for example change the way our DOM patching or hooks work. I'm inclined to wait until this comes up more in the future before committing to it. Thanks! |
Oh, so Hooks added "dynamically" work because the server is sending content. So our problem here is because we are doing this from the client- side, got it 👍 (I wonder if in the future we could do something special for this type of case where one wishes to use templates to render dynamic content).
@josevalim's reply helped, and yours was even better, thanks for the patience @chrismccord! 😉👍 |
@chrismccord I've hit a concrete related usecase that might be interesting here: With infinite scrolling, I want it only to activate after the user first clicked "Load more" (so that scrolling to the footer is still possible). Currently, this is not possible with the built-in phx-viewport bindings because the InfiniteScrolling hook is not loaded when the element updates. In this example clicking "Load more" sets |
By the way, I noticed a difference between user and private hooks: private hooks are initialized when a node is added and before update, and user hooks are initialized when a node is added. Is this on purpose? |
Environment
Elixir 1.14.3 (compiled with Erlang/OTP 25)
1.7.2
0.18.18
Linux debian 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64 GNU/Linux
Actual behavior
Currently, when an element is dinamically added to the DOM and it has a
phx-hook
attribute, the hook is ignored and not run.An example use case where this behavior is useful is implementing the Portal component in the swish component library, which allows an user to mirror an element as a child of another DOM element (such as body), making it easy to create overlaid components, such as dialogs, modals and toasts.
Steps to Reproduce
One can see that after clicking the clone button, an element containing a
phx-hook
attribute is mounted into the DOM but the hook does not run.Expected behavior
After mounting an element that has a
phx-hook
attribute into the DOM, the hook should be executed.It is also interesting to note that other phx attributes, such as
phx-click
continue to work on the dynamically mounted element, whereasphx-hook
doesn´t.The text was updated successfully, but these errors were encountered: