bharatb-futurism
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()">×</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
Marked As Solved
bharatb-futurism
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.
Also Liked
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









