Hey @chirag_23 in short, yes.
This may be a bit of an older technique but I used to do a some fetching of large amounts of content when there was no layout used.
For example, you could utilize Liquid to paginate through at least up to 250 products and then loop through them to capture the available ones and again to capture the unavailable ones. You would fetch the content and then actually paginate & inject client-side. You could either have the JS directly inject product cards from the fetched templates, or by using the product handles and the Section Rendering API to individually fetch product cards of each product before they should be injected. I would recommend whichever approach has better performance.
You may be able to utilize storefront filtering or tag filtering within that approach too or – and I know this is not what you are looking for – you could probably utilize storefront filtering to have “availability” be filterable, they just would not be displayed together at that point. I also believe the pagination limits have recently increased so if you need to work with more than just 1K items you should be able to.
Anyway, yesterday evening I went ahead and started creating a working demo to demonstrate the ability to load in more than 250 products where you only display the unavailable ones at the end.
The following code is just an example using the Dawn theme as a base and probably not the most performant, but it should provide the basis for what you were describing that you were wanting.
Note, it is not a pure-Liquid approach because, as you mentioned with the 250 limit I believe that is currently not doable.
templates/collection.available.liquid
{%- liquid
layout none
capture collection_products
paginate collection.products by 250
assign available_products = collection.products | where: 'available'
for product in available_products
render 'product-card', product: product
echo ',,,'
endfor
endpaginate
endcapture
echo collection_products | remove_last: ',,,'
-%}
templates/collection.cards.liquid
{{ 'template-collection.css' | asset_url | stylesheet_tag }}
{{ 'component-card.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
<script src="{{ 'collection-cards.js' | asset_url }}" defer="defer"></script>
<div
class="product-grid-container"
id="ProductGridContainer"
>
<div class="collection page-width">
<collection-cards
data-collection-url="{{ collection.url }}"
data-products-views="available,unavailable"
data-products-per-page="32"
data-products-count="{{ collection.products_count }}"
data-needle-to-split-by=",,,"
id="product-grid"
class="
grid product-grid grid--2-col-tablet-down
grid--3-col-desktop
"
></collection-cards>
<div>Loading more items…</div>
</div>
</div>
templates/collection.unavailable.liquid
{%- liquid
layout none
capture collection_products
paginate collection.products by 250
for product in collection.products
unless product.available
render 'product-card', product: product
echo ',,,'
endunless
endfor
endpaginate
endcapture
echo collection_products | remove_last: ',,,'
-%}
snippets/product-card.liquid
<div class="grid__item">
{%- render 'card-product',
card_product: product,
media_aspect_ratio: 'adapt',
image_shape: 'default',
show_secondary_image: false,
show_vendor: false,
show_rating: false,
lazy_load: true,
skip_styles: false,
quick_add: false,
section_id: section.id
-%}
</div>
assets/collection-cards.js
if (!customElements.get('collection-cards')) {
customElements.define('collection-cards', class CollectionCards extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.allProducts = [];
this.collectionURL = this.dataset.collectionUrl;
this.currentPage = -1;
this.isLoading = false;
this.loadingElement = this.nextElementSibling;
this.loadingElementBounds = {};
this.needle = this.dataset.needleToSplitBy;
this.onScrollHandler = this.scrollHandler.bind(this);
this.observer;
this.pagedProducts = [];
this.productsPerPage = Number(this.dataset.productsPerPage);
this.productsPerRequest = 250;
this.productsViews = this.dataset.productsViews.split(',');
this.totalProducts = Number(this.dataset.productsCount);
window.performance.mark('theme:collection_loaded.start');
this.initCollection();
}
disconnectedCallback() {
window.removeEventListener('scroll', this.onScrollHandler);
}
chunkArray(array, chunkSize) {
const numberOfChunks = Math.ceil(array.length / chunkSize);
return Array.from({ length: numberOfChunks }, (_, index) => {
const start = index * chunkSize;
const end = start + chunkSize;
return array.slice(start, end);
});
}
async getPage() {
try {
if (this.currentPage < this.pagedProducts.length) {
const products = Array.from(this.pagedProducts[this.currentPage]);
this.insertAdjacentHTML('beforeend', products.join(''));
}
} catch (error) {
console.error(error);
}
}
async initCollection() {
try {
const pageCount = Math.ceil(this.totalProducts / this.productsPerRequest);
for (let i = 1; i <= pageCount; i += 1) {
const response = await fetch(`${window.Shopify.routes.root}${this.collectionURL.replace(/^\//, '')}?view=${this.productsViews[0]}&page=${i}`);
const data = await response.text();
this.allProducts = this.allProducts.concat(data.split(this.needle));
}
if (this.allProducts.length < this.totalProducts) {
for (let i = 1; i <= pageCount; i += 1) {
const response = await fetch(`${window.Shopify.routes.root}${this.collectionURL.replace(/^\//, '')}?view=${this.productsViews[1]}&page=${i}`);
const data = await response.text();
this.allProducts = this.allProducts.concat(data.split(this.needle));
}
}
this.pagedProducts = this.chunkArray(this.allProducts, this.productsPerPage); // Split into array of pages of products.
await this.loadNextPage();
if (window.location.search.includes('page=')) {
const urlParams = new URLSearchParams(window.location.search);
const page = urlParams.get('page');
if (page) {
let tempPage = Number(page) - 1;
if (tempPage < 0) {
tempPage = 0;
}
if (tempPage > 0) {
for (let i = 0; i <= tempPage; i += 1) {
this.currentPage = i;
await this.loadNextPage();
}
}
}
}
if (!!(this.loadingElement)) {
this.loadingElement.textContent = 'Waiting for intersection…';
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.loadingElementBounds = entry.intersectionRect;
if (entry.isIntersecting && !this.isLoading) {
this.loadNextPage();
}
});
}, {
rootMargin: '0px',
threshold: 0.1
});
this.observer.observe(this.loadingElement);
window.addEventListener('scroll', this.onScrollHandler, false);
window.requestIdleCallback(() => {
if (!!(this.loadingElement) && this.isPartiallyInViewport(this.loadingElement)) {
this.loadNextPage.bind(this);
}
})
}
} catch (error) {
console.error(error);
}
performance.mark('theme:collection_loaded.end');
performance.measure('theme:collection_loaded', 'theme:collection_loaded.start', 'theme:collection_loaded.end');
}
isPartiallyInViewport(element) {
if (!!(element)) {
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const isVerticallyVisible = (rect.top < viewportHeight && rect.bottom > 0);
const isHorizontallyVisible = (rect.left < viewportWidth && rect.right > 0);
return isVerticallyVisible && isHorizontallyVisible;
}
return false;
}
async loadNextPage() {
if (this.isLoading) {
return;
}
if (this.currentPage >= this.pagedProducts.length && !!(this.loadingElement)) {
this.loadingElement.textContent = 'No more items to load.';
this.observer.unobserve(this.loadingElement);
return;
}
this.isLoading = true;
if (!!(this.loadingElement)) {
this.loadingElement.textContent = 'Loading more items…';
}
this.currentPage += 1;
await this.getPage();
if (!!(this.loadingElement) && this.currentPage < this.pagedProducts.length) {
this.loadingElement.textContent = 'Waiting for intersection…';
}
if (!!(this.currentPage >= this.pagedProducts.length) && !!(this.loadingElement)) {
this.loadingElement.textContent = 'No more items to load.';
this.observer.unobserve(this.loadingElement);
}
this.isLoading = false;
}
onScrollIdle() {
if (!!(this.loadingElement) && this.isPartiallyInViewport(this.loadingElement)) {
this.isLoading = false;
this.loadNextPage();
}
}
resetIdleTimer() {
clearTimeout(this.idleTimer);
this.idleTimer = setTimeout(this.onScrollIdle.bind(this), 100);
}
scrollHandler() {
if (!!(this.loadingElement) && this.isPartiallyInViewport(this.loadingElement)) {
window.requestAnimationFrame(this.loadNextPage.bind(this));
}
this.resetIdleTimer();
}
});
}
I believe you could adjust the approach to utilize JSON templates without a layout and associated sections instead but for the purposes of this demo I just threw things into Liquid templates. This example also does not mess with the history but it should work if provided a specific actual page within the URL of the collection up-front (without scrolling that “page” into view). I believe I tested it with a new collection that contained 288 active products, and it seemed to work flawlessly as-is.
There is room for improvement, but it is a relatively basic demo to prove what you are wanting to do should be doable without manually reordering or using external apps.