Reputation: 3151
I am trying to display images using this masonry js package but I am having difficulties.
I have an image upload form. The user clicks a button to bind images to Model.Images
, and then they get displayed on the page. The user can bind multiple batches of images before submitting the form. Each time the user binds another batch of images to Model.Images
, the previous batch is persisted - that is to say, I do not truncate Model.Images
before binding the images from the next batch.
The problem I am seeing is: the masonry js script needs to be re-triggered every time Model.Images
gets added to, because the js only applies to the most recently-rendered view. However, it seems every time I force StateHasChanged();
the images from the most recently-added batch overlap the previous images, so I see this:
after first batch of images is bound
after second batch of images is bound
I think this has something to do with differential rendering. If I move the images to a helper object, truncate Model.Images
, force StateHasChanged()
, and then repopulate Model.Images
, the images get displayed side by side properly. I most likely am just approaching this incorrectly. Can someone advise me how to achieve what I want without my hacky implementation? I pasted a simplified version of my code below - this the implementation that is producing the bad results pictured above.
razor component
(the TriggerMasonry()
event gets called after Model.Images
gets appended to.)
@for (int i = 0; i < Model.Images.Count; j++)
{
var iCopy = i; //see https://stackoverflow.com/a/56426146/323447
var uri = $"data:{Model.Images[iCopy].ImageMimeType};base64,{Convert.ToBase64String(Model.Images[iCopy].ImageDataLarge.ToArray())}";
<div class="grid-item" @ref=MasonryElement>
<div class="card m-2 shadow-sm" style="position:relative;">
<img src="@uri" class="card-img-top" alt="..." loading="lazy">
</div>
</div>
}
@code {
ElementReference MasonryElement;
private async void TriggerMasonry()
{
if (Model.Images.Where(x => x.ImageData != null).Any())
{
StateHasChanged();
await JSRuntime.InvokeVoidAsync("initMasonryDelay", MasonryElement);
StateHasChanged();
}
}
}
js
function initMasonryDelay() {
setTimeout(function () {
// init Masonry
var $grid = $('.grid').masonry({
itemSelector: '.grid-item',
percentPosition: true,
columnWidth: '.grid-sizer'
});
// layout Masonry after each image loads
$grid.imagesLoaded().progress(function () {
$grid.masonry();
});
}, 150); //milliseconds
}
css
* {
box-sizing: border-box;
}
.grid:after {
content: '';
display: block;
clear: both;
}
.grid-sizer,
.grid-item {
width: 33.333%;
}
@media (max-width: 575px) {
.grid-sizer,
.grid-item {
width: 100%;
}
}
@media (min-width: 576px) and (max-width: 767px) {
.grid-sizer,
.grid-item {
width: 50%;
}
}
/* To change the amount of columns on larger devices, uncomment the code below */
@media (min-width: 768px) and (max-width: 991px) {
.grid-sizer,
.grid-item {
width: 33.333%;
}
}
@media (min-width: 992px) and (max-width: 1199px) {
.grid-sizer,
.grid-item {
width: 25%;
}
}
@media (min-width: 1200px) {
.grid-sizer,
.grid-item {
width: 20%;
}
}
.grid-item {
float: left;
}
.grid-item img {
display: block;
max-width: 100%;
}
----Another example for user @Brian_Parker----
I tried to apply your suggestions but I'm still not having any luck. I clearly must not understand how JS interoperability works with blazor components. I think my problem has to do with not getting how @ref
works. I sized the array like you suggested, but I'm just creating a proper-length array filled with null refs. I realize this must be wrong, but I am not sure what to fill them with - I tried replacing object[] imageRefs
with a ElementReference[] imageRefs
and filling imageRefs
with ElementReference[i].Id
and passing that to the JS function, but it didn't change the overall behavior of the page. I reverted to baseline for the sake of making my code easier to digest.
I pasted below a different example, that might be easier to understand in a UI context. If you could help me understand what I'm doing wrong here, I'd greatly appreciate it:
I have another component in which I'm trying to apply the Masonry JS to a dynamically-generated list of images. Basic overview: On load, 10 images are pulled from the server and rendered. When the user clicks a "load more" button, 10 more images are pulled from the server and get rendered. I am seeing the same issue where the 2nd round of images is laid over the first round.
My razor component:
<!-- Masonry -->
<div class="grid">
<div class="grid-sizer"></div>
@for (int i = 0; i < pagedThings.Data.Count; i++)
{
var iCopy = i; //see https://stackoverflow.com/a/56426146/323447
<div @key="@pagedThings.Data[iCopy]" class="grid-item" @ref=imageRefs[iCopy]>
<div class="card">
@{
var mimeType = pagedThings.Data[iCopy].ImageThings.FirstOrDefault().ImageMimeTypeLarge;
var imgData = pagedThings.Data[iCopy].ImageThings.FirstOrDefault().ImageDataLarge;
}
<img src="@String.Format("data:" + mimeType + ";base64,{0}", Convert.ToBase64String(imgData))" loading="lazy">
</div>
</div>
}
</div>
<!-- Load more button -->
<button class="btn" type="button" @onclick="GetMoreThings">
<span>Load more</span>
</button>
@code {
PagedResponse<List<Thing>> pagedThings { get; set; }
private int currentPage = 1;
private int currentPageSize = 10;
object[] imageRefs;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
//load Things
await LoadThings();
}
await base.OnAfterRenderAsync(firstRender);
}
private async Task LoadThings()
{
pagedThings = await ThingService.GetByPageAsync($"api/Things/GetThingsByPage?pageNumber={currentPage}&pageSize={currentPageSize}");
imageRefs = new object[pagedThings.Data.Count()];
await JSRuntime.InvokeVoidAsync("masonryAppendDelay", imageRefs[imageRefs.Length - 1]);
StateHasChanged();
}
private async Task GetMoreThings()
{
currentPage++;
var newData = await ThingService.GetByPageAsync($"api/Things/GetThingsByPage?pageNumber={currentPage}&pageSize={currentPageSize}");
foreach (var item in newData.Data)
{
pagedThings.Data.Add(item);
}
imageRefs = new object[pagedThings.Data.Count()];
await JSRuntime.InvokeVoidAsync("masonryAppendDelay", imageRefs[imageRefs.Length - 1]);
StateHasChanged();
}
}
My JS:
function masonryAppendDelay(element) {
setTimeout(function () {
// init Masonry
var $grid = $('.grid').masonry({
itemSelector: element,
percentPosition: true,
columnWidth: '.grid-sizer'
});
// layout Masonry after each image loads
$grid.imagesLoaded().progress(function () {
$grid.masonry();
//$grid.masonry('layout');
});
}, 200); //milliseconds
}
My css is the same as in the original post.
Upvotes: 1
Views: 795
Reputation: 14613
You are capturing a reference in loop. This will only assign the final loops reference. You will have to size an array to capture the references once you know who many iterations there is. refs = new object[someValue]
Your Trigger masonry function will then have to traverse the reference list.
<div @key="@Model.Images[iCopy]" class="grid-item" @ref=refs[iCopy]>
<div class="card m-2 shadow-sm" style="position:relative;">
<img src="@uri" class="card-img-top" alt="..." loading="lazy">
</div>
</div>
@code {
object[] refs;
}
Call your java script for each reference after it has been rendered.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
foreach (var obj in refs)
{
await JSRuntime.InvokeVoidAsync("masonryAppendDelay", obj);
}
}
}
Upvotes: 4