woutdp / live_svelte

Svelte inside Phoenix LiveView with seamless end-to-end reactivity
https://hexdocs.pm/live_svelte
MIT License
1.01k stars 37 forks source link

Whenever assign changes the svelte component is rerendered and the input element loses focus #84

Closed morfert closed 8 months ago

morfert commented 8 months ago

Not sure if I am using this wrong but whenever I change the value in an assign which is being passed in as a prop, the svelte component rerenders triggering the onMount.

def render(assigns) do
  ~H"""
    <%!-- ... --%>
    <.svelte name="Auto" socket={@socket} props={%{value: @value}} />
    <%!-- ... --%>
  """
end

I have an input in the svelte component that sends an event whenever the value changes.

<script>
  // ...
  import { onMount } from "svelte";

  export let live;
  export let value;

  onMount(()=> console.debug(value))

  function update(value) {
    live.pushEvent("validate", { value: value }, () => { });
  }

  $: update(value)
  // ...
// ...
</script>
<!-- ... -->
<input type="text" bind:value />
<!-- ... -->

The handle_event on the server changes the assigns as the input is typed into, but the input loses focus since the component is rerendered.

def handle_event("validate", %{value: value}, socket) do
  value = value |> validate()
  socket = socket |> assign(value: value)

  {:noreply, socket}
end
woutdp commented 8 months ago

I have your code running successfully with some modifications:

  1. I'm using %{"value" => value} instead of %{value: value} in the handle_event("validate", ...) function.
  2. I added a validating variable to indicate when someting is validating. This is to prevent an infinite loop of variables being reset. Because of the reactivity of Svelte and LiveSvelte with the computed update this can happen when typing too fast. Probably better to do something with a debounce or throttle but this solves that issue for now.
defmodule ExampleWeb.Example do
  use ExampleWeb, :live_view

  def render(assigns) do
    ~V"""
    <script>
      import { onMount } from "svelte";

      export let live;
      export let value;

      let validating = false

      onMount(async () => {
        console.log(value)
      })

      function update(value) {
        if (validating) return
        validating = true
        live?.pushEvent("validate", {value: value}, () => {validating = false});
      }

      $: update(value)
    </script>

    <input type="text" bind:value />
    """
  end

  def mount(_params, _session, socket) do
    {:ok, assign(socket, value: nil)}
  end

  def handle_event("validate", %{"value" => value}, socket) do
    {:noreply, assign(socket, value: value)}
  end
end

I did not get your issue with the onMount, but these things might have caused it for you with additional code.

Let me know if this resolves it.

morfert commented 8 months ago

I recreated what you provided and it works as expected. I had the svelte component in an inputs_for and the event also caused the inputs_for to rerender which I did not mention before. Today, I discovered phx-update which seems to let me sync state in assigns while controlling when the svelte component rerenders. I am sorry for not providing the full context, I had assumed that, without testing, the above example would recreate the issue. I have no excuse for my laziness. Sorry for wasting time.

woutdp commented 8 months ago

No worries, I'm happy it works and this package is useful to you :)