Implementing a Responsive Masonry Layout with jQuery Wookmark

To achieve a responsive waterfall (masonry) layout, the primary distinction between mobile and desktop configurations lies in the item width. Mobile views utilize percentage-based widths for fluidity, whereas desktop views typically rely on fixed pixel widths. This guide demonstrates how to implement Wookmark with infinite scrolling and AJAX loading, while addressing a common overlap issue caused by image loading timing.

Dependencies

Include the necessary CSS and JavaScript libraries in your document header. This includes the Wookmark plugin, jQuery, and the imagesLoaded plugin to ensure layout calculations wait for image assets.

<link rel="stylesheet" href="path/to/reset.css" />
<link rel="stylesheet" href="path/to/main.css" />

<script src="path/to/jquery.min.js"></script>
<script src="path/to/jquery.imagesloaded.js"></script>
<script src="path/to/jquery.wookmark.js"></script>

Markup Structure

Create a container element to hold the list of items and a separate section for the loading indicator and end-of-list message.

<div id="main-container" role="main">
    <ul id="masonry-grid">
        <!-- Dynamic content will be injected here -->
    </ul>
    
    <div id="loading-indicator">
        <div id="spinner"></div>
        <div class="end-message" style="display:none;">
            No more items to display.
        </div>
    </div>
</div>

Frontend Logic

The JavaScript handles the infinite scroll event, fetches data via AJAX, and updates the grid layout. A critical fix is implemented here: new elements are appended to the DOM *before* the layout engine runs. If the layout is calculated while elements are detached or images are not yet in the DOM, height calculations will fail, causing items to overlap.

(function ($) {
    const $gridContainer = $('#masonry-grid');
    const $window = $(window);
    const $document = $(document);
    const apiEndpoint = 'api/Gallery/GetData';
    
    let currentPage = 1;
    let isFetching = false;
    let lastFetchTime = 0;

    // Configuration for the Wookmark layout
    const layoutConfig = {
        autoResize: true,
        container: $gridContainer,
        offset: 2,
        itemWidth: '49%' // Use percentage for responsive mobile design
    };

    /**
     * Handles scroll events to trigger infinite loading.
     */
    function handleScroll() {
        if (!isFetching) {
            const scrollThreshold = 100;
            const nearBottom = ($window.scrollTop() + $window.height() > $document.height() - scrollThreshold);
            
            if (nearBottom) {
                const now = new Date().getTime();
                // Throttle requests to one per second
                if (lastFetchTime < now - 1000) {
                    lastFetchTime = now;
                    fetchItems();
                }
            }
        }
    }

    /**
     * Refreshes the masonry layout with new items.
     */
    function updateLayout($newItems) {
        // Crucial: Append to DOM before calculating layout to prevent overlap
        $gridContainer.append($newItems);

        // Wait until images are loaded to calculate dimensions
        $gridContainer.imagesLoaded(function () {
            const $allItems = $('li', $gridContainer);
            
            // Destroy previous layout instance
            if ($allItems.data('wookmarkInstance')) {
                $allItems.data('wookmarkInstance').clear();
            }

            // Initialize new layout
            $allItems.wookmark(layoutConfig);

            // Fade in new elements
            $newItems.animate({ opacity: 1 }, 300);
        });
    }

    /**
     * Fetches JSON data from the server.
     */
    function fetchItems() {
        isFetching = true;
        $('#spinner').show();

        $.ajax({
            url: apiEndpoint,
            type: 'POST',
            dataType: 'json',
            data: JSON.stringify({ "pageNumber": currentPage }),
            contentType: 'application/json; charset=utf-8',
            success: handleDataResponse,
            error: function() {
                alert("Request failed or rate limited.");
                isFetching = false;
                $('#spinner').hide();
            }
        });
    }

    /**
     * Processes the server response and renders HTML.
     */
    function handleDataResponse(response) {
        isFetching = false;
        $('#spinner').hide();
        currentPage++;

        const items = response.d.Data;
        let htmlBuffer = '';

        items.forEach(item => {
            htmlBuffer += '<li style="width: 49%;">';
            htmlBuffer += '<a href="#" title="' + item.Name + '">';
            htmlBuffer += '<img src="' + item.ImageUrl + '" style="width:100%" alt="Item" />';
            htmlBuffer += '</a>';
            htmlBuffer += '<div class="meta-info">';
            htmlBuffer += '<span class="id-tag">ID: ' + item.Number + '</span>';
            htmlBuffer += '<span class="vote-count">' + item.VoteCount + ' votes</span>';
            htmlBuffer += '</div>';
            htmlBuffer += '<button class="action-btn">Vote</button>';
            htmlBuffer += '</li>';
        });

        const $newElements = $(htmlBuffer);

        // Check if we reached the end of the list
        if (response.d.Msg === 'NoMore') {
            $document.off('scroll', handleScroll);
            $('.end-message').show();
        }

        updateLayout($newElements);
    }

    // Initialize scroll listener and load first page
    $document.on('scroll', handleScroll);
    fetchItems();

})(jQuery);

Backend Data Source (C#)

The backend method generates mock data for the gallery. It simulates pagination and returns a flag when the data source is exhausted.

 5)
    {
        response.IsSuccess = true;
        response.Message = "NoMore";
        return response;
    }

    var itemList = new List<object>();

    // Generate mock data items
    for (int i = 0; i < 5; i++)
    {
        itemList.Add(new
        {
            Number = i + ((pageNumber - 1) * 5),
            Name = "Gallery Item",
            VoteCount = 20 + i,
            ImageUrl = "/images/sample1.png"
        });
    }

    for (int j = 5; j < 10; j++)
    {
        itemList.Add(new
        {
            Number = j + ((pageNumber - 1) * 5),
            Name = "Gallery Item",
            VoteCount = 50 + j,
            ImageUrl = "/images/sample2.jpg"
        });
    }

    response.Payload = itemList;
    response.IsSuccess = true;
    response.Message = "Success";
    
    return response;
}

public class GalleryResponse
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }
    public object Payload { get; set; }
    public object Data { get; set; } // Added for compatibility with JS parsing
}

Tags: jquery Wookmark Masonry Infinite-Scroll Ajax

Posted on Mon, 18 May 2026 11:33:38 +0000 by Naez