LiveView re-render is causing DOM elements to reset

I am dealing with an issue where a LiveView re-render is causing DOM elements to reset, which includes clearing a div that has a base64 encoded src attribute. When working with Phoenix LiveView, once we enter the prompt, LiveView updates the DOM based on the server state, and any client-side changes that are not reflected in the server state, the custom image is not being selected and gets disappear.

  def render(assigns) do
    ~H"""
    <img
      id="hiddenImage"
      style="display:none;"
      alt="Hidden Image"
      width="250px"
      height="250px"
      style="border-radius:2px;border:1px solid grey;"
      src={@image_base64}
    />
    <div id="prompt-editor">
      <div class="mx-auto max-w-6xl grid grid-cols-1 md:grid-cols-3 gap-x-16 mt-6 mb-12">
        <div class="col-span-2 relative mx-4">
          <a
            href="/recipes/all"
            class="absolute -top-8 inline-block h-5 justify-center items-center gap-2 inline-flex  cursor-pointer"
          >
            <.icon name="hero-arrow-left" class="w-5 h-5" />
            <span class="text-slate-600 text-sm font-semibold leading-tight">
              Back to Recipes
            </span>
          </a>
          <.h1 class="text-5xl font-bold"><%= @recipe.name %></.h1>
          <div class="flex justify-between items-center">
            <.support class="text-xl">
              <%= gettext("Fill in the blanks of your recipe to generate art") %>
            </.support>
            <%!-- <.hollow_button
            disabled={@generating}
            class={if @generating, do: [extend: "opacity-50"], else: [extend: ""]}
          >
            <%= gettext("Surprise me!") %>
          </.hollow_button> --%>
          </div>
          <%= if @recipe.upload_capture_image do %>
            <div class="mt-6">
              <label for="upload-image" class="block text-sm font-medium text-gray-700">
                Upload Image
              </label>
              <input type="file" id="fileInput" accept="image/*" />
              <br /><br />
              <img
                id="previewHolder"
                alt="Uploaded Image Preview"
                width="250px"
                height="250px"
                style="border-radius:2px;border:1px solid grey;"
                src={@image_base64}
              />
              <div class="mt-4 text-center ml-6">
                <a href="#" class="text-blue-500 hover:underline" onclick="openCamera()">
                  Capture Image from Camera
                </a>
              </div>
            </div>
            <div id="myModal" class="modal">
              <div class="modal-content">
                <span class="close" onclick="closeModal()">&times;</span>
                <div class="video-container">
                  <video id="video" autoplay></video>
                </div>
                <button class="capture-button" onclick="captureImage()">Capture</button>
                <canvas id="canvas" style="display: none;"></canvas>
              </div>
            </div>
          <% end %>
          <.form
            phx-submit="save"
            phx-change="change"
            for={%{}}
            class={if @generating, do: "opacity-50", else: ""}
            id="bharat"
          >
            <div phx-remove="append" id="edit-prompt-owner-id" phx-hook="ImageHook">
              <input type="hidden" name="owner_id" phx-hook="ShopifyID" id="owner-id" />
            </div>
            <.big_body_text>
              <.madlib_editor
                chunks={@chunks}
                madlib={Recipe.split_madlib(@recipe.madlib)}
                reset_key={@reset_key}
              />
              <input type="hidden" id="image_base64" name="image_base64" value="" />
              <.button
                disabled={@generating}
                class={[
                  "inline-block h-12 min-w-[6rem] text-center",
                  if(@generating, do: "bg-slate-600", else: "")
                ]}
              >
                <%= if @generating do %>
                  <svg
                    class="animate-spin h-5 w-5 text-white inline"
                    xmlns="http://www.w3.org/2000/svg"
                    fill="none"
                    viewBox="0 0 24 24"
                  >
                    <circle
                      class="opacity-25"
                      cx="12"
                      cy="12"
                      r="10"
                      stroke="currentColor"
                      stroke-width="4"
                    >
                    </circle>
                    <path
                      class="opacity-75"
                      fill="currentColor"
                      d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                    >
                    </path>
                  </svg>
                <% else %>
                  <.icon name="hero-sparkles" class="mr-2" /> <%= gettext("Generate") %>
                <% end %>
              </.button>
            </.big_body_text>
          </.form>
        </div>

        <script>
            let ImageHook = {
              mounted() {
                const storedImage = localStorage.getItem('image_base64');
                if (storedImage) {
                  document.getElementById('previewHolder').setAttribute('src', storedImage);
                }
              },
              updated() {
                const storedImage = localStorage.getItem('image_base64');
                if (storedImage) {
                  document.getElementById('image_base64').value=storedImage;
                  document.getElementById('previewHolder').setAttribute('src', storedImage);
                }
              }
            };
            document.addEventListener('DOMContentLoaded', function () {
                const storedImage = localStorage.getItem('image_base64');
                if (storedImage) {
                  document.getElementById('hiddenImage').value=storedImage;
                  document.getElementById('previewHolder').setAttribute('src', storedImage);
                  document.getElementById('image_base64').value = storedImage;
                }
            });
            document.getElementById('fileInput').addEventListener('change', function () {
                readURL(this);
            });

            function readURL(input) {
              if (input.files && input.files[0]) {
                var reader = new FileReader();
                reader.onload = (e) => {
                  const base64String = e.target.result;
                  document.getElementById('previewHolder').setAttribute('src', base64String);
                  document.getElementById('hiddenImage').setAttribute('src', base64String);
                  compressImageFromFile(e.target.result, 4096).then(base64String => {
                      document.getElementById('image_base64').value = base64String;
                      localStorage.setItem('image_base64', base64String);
                      console.log(base64String);
                  });
                };
                reader.readAsDataURL(input.files[0]);
            } else {
              document.getElementById('hiddenImage').setAttribute('src', base64String);
                //alert('Select a file to see preview');
            }
          }
            let video = document.getElementById('video');
            let canvas = document.getElementById('canvas');
            let modal = document.getElementById('myModal');
            function openCamera() {
              modal.style.display = "block";
              navigator.mediaDevices.getUserMedia({ video: true })
                .then(stream => {
                  video.srcObject = stream;
                })
                .catch(err => {
                  console.log("An error occurred: " + err);
                });
            }
            function closeModal() {
              modal.style.display = "none";
              let stream = video.srcObject;
              let tracks = stream.getTracks();

              tracks.forEach(track => {
                track.stop();
              });

              video.srcObject = null;
            }
          function captureImage() {
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            let context = canvas.getContext('2d');
            context.drawImage(video, 0, 0, canvas.width, canvas.height);

            let base64Image = canvas.toDataURL('image/jpeg');
            console.log('Captured Image in Base64:', base64Image);

            compressImageFromFile(base64Image, 4096).then(base64String => {
              document.getElementById('previewHolder').setAttribute('src', base64String);
              document.getElementById('hiddenImage').setAttribute('src', base64String);
              document.getElementById('image_base64').value = base64String;
              localStorage.setItem('image_base64', base64String);
              console.log(base64String);
            });

            closeModal();
          }
            function compressImageFromFile(dataUrl, maxLength) {
              return new Promise((resolve) => {
                var img = new Image();
                img.onload = function () {
                    var canvas = document.createElement('canvas');
                    var context = canvas.getContext('2d');
                    var width = img.width;
                    var height = img.height;

                    // Calculate the new dimensions
                    var ratio = Math.min(1, Math.sqrt(maxLength / (width * height)));
                    width = width * ratio;
                    height = height * ratio;

                    canvas.width = width;
                    canvas.height = height;
                    context.drawImage(img, 0, 0, width, height);

                    var base64String = canvas.toDataURL('image/jpeg', 0.7); // Adjust the quality to compress further if needed

                    // If the compressed image is still too large, reduce the quality further
                    while (base64String.length > maxLength && quality > 0.1) {
                        quality -= 0.1;
                        base64String = canvas.toDataURL('image/jpeg', quality);
                    }

                    resolve(base64String);
                };
                img.src = dataUrl;
              });
            }
        </script>

        <div class="col-span-1 mx-4">
          <.image_loading :if={@generating} />
          <div :if={!@generating}>
            <div :if={!@image.id} class="grid grid-cols-2 gap-4" id="preview-images">
              <%= for {image, index} <- Recipes.get_featured_preview_images(@recipe, 4) |> Enum.with_index() do %>
                <.recipe_preview_image
                  id={"preview-image-#{@recipe.id}-#{index}"}
                  src={
                    Roboart.File.path_to_full_url(image.cached_image_url) || image.original_image_url
                  }
                  alt={image.prompt}
                  image={image}
                  recipe={@recipe}
                  href={
                    if image.id,
                      do: ~p"/recipes/#{@recipe.slug}/images/new?image=#{image.id}",
                      else: ~p"/recipes/#{@recipe.slug}/images/new"
                  }
                />
              <% end %>
            </div>
            <div :if={@image.id} id="selected-image">
              <div :if={@output} class="grid grid-cols-4 gap-4 pb-4" id="preview-images">
                <%= for {image, index} <- @output |> Enum.with_index() do %>
                  <.recipe_preview_image
                    id={"preview-image-#{@recipe.id}-#{index}"}
                    src={
                      Roboart.File.path_to_full_url(image.cached_image_url) ||
                        image.original_image_url
                    }
                    alt={image.prompt}
                    image={image}
                    recipe={@recipe}
                    selected={image == @image}
                    event="select_image"
                  />
                <% end %>
              </div>
              <.recipe_preview_image
                id={"selected-image-#{@recipe.id}"}
                src={
                  Roboart.File.path_to_full_url(@image.cached_image_url) || @image.original_image_url
                }
                href="#"
                alt={@image.prompt}
                image={@image}
                recipe={@recipe}
              />
              <div class="mt-6">
                <.cta_button
                  click={JS.navigate(~p"/images/#{@image.id}/edit")}
                  class={[extend: "block w-full"]}
                >
                  Print this design <.icon name="hero-arrow-right" />
                </.cta_button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <.live_component
        module={CommunityGallery}
        id="community-gallery"
        images={if @show_my_images, do: @my_images, else: @recipe.images}
        recipe={@recipe}
        show_my_images={@show_my_images}
      />
    </div>
    """
  end

    <%= for chunk <- @madlib do %>
      <%= if Recipe.variable_chunk?(chunk) do %>
        
        <.madlib_input
          id={"chunk_#{Recipe.chunk_to_key(chunk)}-#{@reset_key}"}
          label={Recipe.label(chunk)}
          name={"chunk[#{Recipe.chunk_to_key(chunk)}]"}
          value={String.trim(Map.get(@chunks, Recipe.chunk_to_key(chunk), ""))}
        />
      <% else %>
        <%= chunk %>
      <% end %>
    <% end %>

Due to this loop every time when we enter a character image got cleared from

<img
          id="previewHolder"
          alt="Uploaded Image Preview"
          width="250px"
          height="250px"
          style="border-radius:2px;border:1px solid grey;"
          src={@image_base64}
        />

Please suggest

Hello and welcome!

If you want to manage any client-state on the client, you need to add phx-update="ignore" to the parent of whatever you want left alone. You can read more here.

2 Likes

You are right but for that It was need to create a div element which will have phx-update=“ignore” and inside the div it is needed to keep hidden field and preview image field so that DOM will be updated but due the div imagepreview field will be ignored.