1

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 first batch of images is bound

after second 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.

sion_corn
  • 3,043
  • 8
  • 39
  • 65
  • You are reszing imageRefs = new object[pagedThings.Data.Count()]; but not populating it with any data. I am also not understang how imageRefs is being applied. – Alexander Higgins Nov 18 '20 at 04:30
  • Hey @sion_corn do you have an overview of a complete blazor implementation using masonry js? ( a demo site I mean ) I would love to use it to. – Depechie May 27 '21 at 08:24

1 Answers1

4

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);
        }
    }
}

Brian Parker
  • 11,946
  • 2
  • 31
  • 41
  • thanks for the suggestion, but this does not make a difference in my case. – sion_corn Nov 16 '20 at 16:18
  • The problem seems to come from the JS - it seems to only work if it is called once: `await JSRuntime.InvokeVoidAsync("initMasonryDelay");` - if I call it for the first time only after the **second** image is bound, then both images are neatly arranged in a masonry layout. if I call the JS each time an image gets bound, then I see the overlap issue. – sion_corn Nov 16 '20 at 16:32
  • @sion_corn I think I have spotted the issue. – Brian Parker Nov 16 '20 at 17:22
  • @brian_parker thanks for your reply. I still don't quite get it - I updated the OP with another example that shows my attempt to implement your suggestions. Unfortunately I am still unable to produce the behavior that I'm after. – sion_corn Nov 16 '20 at 22:44
  • @sion_corn You need to call your JavaScript for each reference. The changes you made still only call it once and for the last item. – Brian Parker Nov 17 '20 at 13:29
  • i was able to get it to work following your advice - thanks. – sion_corn Nov 18 '20 at 14:17
  • hey @BrianParker I'm also trying to get this going... but just starting with Blazor, I'm stuck with your solution example ( as in, not sure how to get that refs[iCopy] variable initialised correctly ). I'm trying to get this codepin of mine converted to blazor: https://codepen.io/depechie/pen/xxqdJdW could you maybe expand the code example a bit more? – Depechie Jun 01 '21 at 07:06
  • 1
    @Depechie Can you put your blazor code in a BlazorRepl.com or somewhere similar and I'll help. – Brian Parker Jun 01 '21 at 08:48