Skip to content
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

It's not possible to have nested stateful LiveVue components inside a LiveVue parent component #14

Open
vheathen opened this issue Jun 9, 2024 · 2 comments

Comments

@vheathen
Copy link

vheathen commented Jun 9, 2024

Seems that currently it's not possible to have nested LiveVue components added via slots

Here is an example:

  def render(assigns) do
    ~H"""
    <div class="w-[350px] flex flex-col text-center h-full justify-center gap-2">
      <div>
        <%!-- Top level component --%>
        <.Card class="m-5">
          <div class="p-10 flex flex-col text-center h-full justify-center gap-2">

            <p id="with-socket">
              <%!-- Nested component with socket, goes away after mount --%>
              <.Counter count={@count} v-socket={@socket} id="inner-with-socket" v-on:inc={JS.push("inc_counter")} />
            </p>

            <p id="without-socket">
              <%!-- Nested component without socket, shown, but static, doesn't have even a local state --%>
              <.Counter count={@count} id="inner-without-socket" v-on:inc={JS.push("inc_counter")} />
            </p>

          </div>
        </.Card>
      </div>

      <%!-- Another top level component --%>
      <div>
        <.Counter count={@count} v-socket={@socket} id="outer-with-socket" v-on:inc={JS.push("inc_counter")}  />
      </div>
    </div>
    """
  end

Card is a very simple Vue component which has only a slot inside:

<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('rounded-xl border bg-card text-card-foreground shadow', props.class)">
    <slot />
  </div>
</template>

The very first Counter.vue disappears (this is slightly visually modified module included to LiveVue) disappears after the view mounted to the server.

The second one is there, but it doesn't have any state or behaviour - event the slider itself doesn't change the button label.

The third one - which is outside of the Card - is working as intended.

image

Funny enough that the top visible Counter gets one update when the server state changes the first time (the first "Increase counter" click) but then it stops.

I tried to look into the code but haven't found a good way to implement the case above.

Also, I noticed that each component gets its own application instance. I wonder if it makes sense to have only one app? In this case components can share state. Or it can be configurable. Don't think it worth spending time on that now TBH, but in the future can be interesting.

But the question is how should nested Vue components behave is an interesting topic. Currently to change server state from the nested Vue components (I mean, actual Vue components without anything in-between) it is necessary to emit events from the bottom to the top level one, which is actually got a VueHook, and then this component will emit an event to the LiveView controller.

May be an option here is to implement a server-side reactive store of some kind? It is possible to integrate pinia, but it will be too heavy only for that purpose, I think.

Anyway, I believe this topic worth another thread :-D

@Valian
Copy link
Owner

Valian commented Jun 9, 2024

@vheathen Great points! I'll try to explain why it looks as it looks:

  1. You can't mount standalone Vue component into a page. It always has to be wrapped by top-level app created with createApp. I don't see a way of having a single Vue app mounted into multiple places (maybe portals? but it would be quite complex and might cause bugs).

  2. To keep slots interactive, they're fully rendered on the server and sent as an HTML over the wire. In other words, they doesn't use phoenix-driven update & render cycle since I couldn't find a way of making it work (once Vue "takes over" rendering phoenix can't update & execute hooks inside). So currently content in slots can't use phoenix hook - it's the same as in LiveSvelte. If you could find a way to make it work, it would be amazing :)

  3. To emit events directly to live view from a nested compontent you can use

import {useLiveVue} from "live_vue"

const live = useLiveVue()

live.pushEvent("ping")

or you can subscribe to handle custom events as well. useLiveVue gives you the hook instance, so you can use anything from JS interoperability.

  1. I was thinking about exposing a state component that would synchronize passed props to reactive object on the frontend, so it could be shared by multiple components, if necessary. But right now it's just a thought 😉

@Valian
Copy link
Owner

Valian commented Nov 23, 2024

@vheathen maybe phoenixframework/phoenix_live_view#3478 might be a way to solve this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants