Pagination

Add pagination to a publish extension.

In a publish extension, there's a limit to how many resources — images or containers — can be returned in a single response. This limit ensures that resources are loaded quickly.

To provide users with more resources than this limit allows, they need to be loaded across multiple requests. In some APIs, this is known as pagination. In the publish extension API, it's known as continuation. The terms, however, can be used interchangeably.

The best way to understand pagination is with a practical example, so this topic explains how to allow users to paginate through an arbitrary list of resources.

For a publish extension to support pagination, it must use the Flat list or Nested list layout.

Prerequisites

This tutorial assumes experience with:

  • REST APIs

  • Node.js

  • Express.js

You should have also completed the quick start for publish extensions.

Set up

Before continuing:

  1. Create an app via the Developer Portal.

  2. Add a content extension to the app.

  3. Set up a REST API with Glitch and Express.js:

    const express = require('express');
    const app = express();
    app.use(express.json());
    app.use(express.static('public'));
    // Routes go here
    app.listen(process.env.PORT || 3000);

Loading the initial page of resources

When a user opens a publish extension, Canva sends a POST request to /publish/resources/find:

app.post('/publish/resources/find', async (request, response) => {
console.log(request.body);
});

The purpose of this request is to retrieve that resources that will appear in the Publish menu.

For this tutorial, use the fill and map methods to generate an array of "CONTAINER" resources:

app.post('/publish/resources/find', async (request, response) => {
const containers = new Array(20).fill(0).map((item, index) => {
return {
type: 'CONTAINER',
id: `${index}`,
name: `Container #${index}`,
isOwner: true,
readOnly: false,
};
});
response.send({
type: 'SUCCESS',
resources: containers,
});
});

You cannot return more than 100 resources in a single response.

Providing the next page number in the response

Before a user reaches the end of the resource list, we want to trigger an additional request to /publish/resources/find. This will allow us to seamlessly load additional resources, creating the effect of infinite scrolling.

To accomplish this, add a continuation property to the response:

response.send({
type: 'SUCCESS',
resources: images,
continuation: '2',
});

The presence of this property tells Canva that it should send another POST request to /publish/resources/find before the user reaches the end of the resource list. The value of this property will also be included in the body of that request.

In this example, the value of the continuation property is "2". This is because we've loaded the first page of results and we would like to load the second page. But this value does not have to be a page number. The only requirement is that this value is a string.

After making this change, scroll through the resources in the side panel.

The same resources will be repeatedly loaded. This isn't exactly what we want, but it's a step in the right direction.

Getting the current page number from the request body

When a continuation property is included in a response, that same continuation property will be made available in the body of the next POST request that's sent to /publish/resources/find.

To see an example of this, log the value of request.body.continuation:

console.log(request.body.continuation);

When the first page of resources is loaded, the continuation property is null. This is because there isn't a previous response. But if you trigger another request by scrolling through the resources, the value of "2" will be logged. You can use this behavior to load unique resources for each request.

At the top of the route, before the options object, create a currentPage variable that contains the value of the request body's continuation property:

const currentPage = request.body.continuation;

But since the continuation property will be null on the initial request, provide a fall-back value of "1":

const currentPage = request.body.continuation || '1';

This code will include the page number in the name of the resources:

const resources = new Array(20).fill(0).map((item, index) => {
return {
type: 'CONTAINER',
id: `${currentPage}-${index}`,
name: `Page #${currentPage}, Container #${index}`,
isOwner: true,
readOnly: false,
};
});

Based on these changes, we'll be able to load the first and second pages into the side panel.

If we keep scrolling through the resources though, it's now the second page of images that will be repeatedly loaded. This is because we've hard-coded the value of the continuation property in all responses as "2".

Even so, this is another step in the right direction.

Incrementing the page number

To load unique resources for every request, we need to increment the value of the continuation property.

Before the response.send method, create a variable that parses the number from the currentPage variable, increments it by one, and converts the result back into a string:

const nextPage = (parseInt(currentPage, 10) + 1).toString();

Then provide this variable in the response:

response.send({
type: 'SUCCESS',
resources: images,
continuation: nextPage,
});

Based on this change, a unique set of resources will be loaded for each request.

Loading the final page of resources

When the final page of resources is reached, don't provide a continuation property in the response:

response.send({
type: 'SUCCESS',
resources: images,
});

…or set the continuation property to null:

response.send({
type: 'SUCCESS',
resources: images,
continuation: null,
});

This signifies that there's no more pages to load.

Example

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.post('/publish/resources/find', async (request, response) => {
const currentPage = request.body.continuation || '1';
const nextPage = (parseInt(currentPage, 10) + 1).toString();
const maxPages = 5;
const resources = new Array(20).fill(0).map((item, index) => {
return {
type: 'CONTAINER',
id: `${currentPage}-${index}`,
name: `Page #${currentPage}, Container #${index}`,
isOwner: true,
readOnly: false,
};
});
response.send({
type: 'SUCCESS',
resources,
continuation: currentPage < maxPages ? nextPage : null,
});
});
app.listen(process.env.PORT || 3000);