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