Recent searches

in

Common patterns for the Visual Editor API

Last modified: August 11th, 2026

On this page

The Visual Editor API is designed for experienced CloudCannon developers. If you need help with custom integrations or your CloudCannon setup in general, please contact our friendly support team.

CloudCannon's Visual Editor API exposes files, Collections, and Datasets so you can build custom integrations in the Visual Editor. This article collects common patterns you can adapt when building those integrations.

This article assumes you know how to initialize the Visual Editor API and listen for changes. For a complete list of API objects and methods, please read our reference documentation on the Visual Editor API.

Get the API Object#

Each pattern below assumes you have a v1 API Object available as api. The following shows one way to obtain it.

JavaScript
Copied to clipboard
const apiPromise = new Promise((resolve) => { 
  if (window.CloudCannonAPI) {
    const api = window.CloudCannonAPI.useVersion('v1', true);
    resolve(api);
  } else {
    document.addEventListener( 
      'cloudcannon:load',
      () => {
        const api = window.CloudCannonAPI.useVersion('v1', true);
        resolve(api);
      },
      { once: true }
    );
  }
});

(async () => { 
  const api = await apiPromise;
  // Custom integration code here
})();

Create a Promise that resolves with the v1 API Object. If window.CloudCannonAPI is already present when your script runs, it resolves immediately. Passing true as the second argument to useVersion() prevents the API Object from being assigned to window.CloudCannon.

If the API Object is not present, the Promise waits for the cloudcannon:load event.

Await apiPromise inside an async IIFE. The api variable holds the v1 API Object for use in your integration code.

Filter files#

Because *.items() returns an array, you can filter it using standard JavaScript. This works for Collections and folder-based Datasets.

JavaScript
Copied to clipboard
const posts = api.collection('posts'); 
const files = await posts.items(); 
const published = []; 

for (const file of files) {
  const data = await file.data.get();
  if (data?.published) published.push(file); 
}

Create a posts variable to access the posts Collection.

Call posts.items() to get an array of file objects in the Collection.

Create an empty array to hold the filtered results.

Read the structured data for each file, and add files with a published field set to true to the results array.

JavaScript
Copied to clipboard
const locales = api.dataset('locales'); 
const files = await locales.items(); 
const active = []; 

for (const file of files) {
  const data = await file.data.get();
  if (data?.active) active.push(file); 
}

Create a locales variable to access the locales Dataset. This example assumes the Dataset is configured as a folder, so locales.items() returns an array.

Call locales.items() to get an array of file objects in the Dataset.

Create an empty array to hold the filtered results.

Read the structured data for each file, and add files with an active field set to true to the results array.

Re-render when data changes#

A common pattern is to re-render your integration whenever data changes. Listening for both change and delete events keeps your integration in sync regardless of what the Team Member does.

JavaScript
Copied to clipboard
const refresh = async () => { 
  const file = api.currentFile();
  const data = await file.data.get();
  renderPreview(data);
};

api.addEventListener('change', refresh); 
api.addEventListener('delete', refresh); 

Define a refresh function that reads the current file's data and re-renders your integration. renderPreview is a placeholder for whatever rendering your integration does, such as updating a live preview or refreshing a component tree.

Call refresh whenever data is created or updated.

Call refresh whenever a file is deleted. This handles cases where the current file is removed while open in the Visual Editor.

Debounce a re-render#

If your integration re-renders frequently (for example, on every keystroke), debounce the handler to avoid unnecessary work.

JavaScript
Copied to clipboard
const debounce = (fn, ms) => { 
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
};

const refresh = debounce(async () => { 
  const file = api.currentFile();
  const data = await file.data.get();
  renderPreview(data);
}, 200);

api.addEventListener('change', refresh);

A simple debounce utility that delays execution until the function stops being called for a given number of milliseconds.

Wrap refresh with debounce() so it only fires after 200ms of inactivity, rather than on every individual change event.

Read the file that changed#

change events on the API Object and on a Collection or Dataset include event.detail.sourcePath, the path of the file that changed, and event.detail.isNew, which is true for newly created files. Use sourcePath to load the file that changed and read its data. File level events carry the same detail fields, but a file level listener already targets one known file, so you can read that file directly instead of using sourcePath.

JavaScript
Copied to clipboard
api.addEventListener('change', async (event) => { 
  const pathToChangedFile = event.detail.sourcePath; 
  const changedFileIsNew = event.detail.isNew; 

  const file = api.file(pathToChangedFile); 
  const newFileContent = await file.data.get();
  console.log(newFileContent);
});

Listen for change events across your entire Site. Mark the callback async so you can await file methods inside it.

event.detail.sourcePath is the path of the file that changed, relative to your Site root.

event.detail.isNew is true when the file was newly created, and false when an existing file was updated.

Pass sourcePath to api.file() to load the file that changed, then read its data with file.data.get().

JavaScript
Copied to clipboard
const posts = api.collection('posts'); 

posts.addEventListener('change', (event) => { 
  const pathToChangedFile = event.detail.sourcePath;
  const changedFileIsNew = event.detail.isNew;
});

Create a posts variable to access the posts Collection. Dataset events carry the same detail fields.

As with root level events, event.detail.sourcePath is the path of the file that changed, and event.detail.isNew is true for newly created files and false for updates.

JavaScript
Copied to clipboard
const file = api.currentFile(); 

file.addEventListener('change', async (event) => { 
  const newFileContent = await file.data.get();
  console.log(newFileContent);
});

Create a file variable to access the file currently open in the Visual Editor.

A file level listener already targets one known file, so you don't need event.detail.sourcePath. Because you already have the file reference, read its data directly with file.data.get().

Related Resources

Open in a new tab