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 CloudCannon's Visual Editor API is a public JavaScript API for building custom integrations with the Visual Editor. It lets you run your own code inside the Visual Editor to read and write content, listen for changes, and upload files using code running inside your website files. A group of related files with a similar format (e.g., a folder of pages, blog posts, or data files). Once you group your files into Collections, they appear in the Site Navigation for easy access. A structured data file or folder defined under CloudCannon's visual editing interface provides a user-friendly way of updating your website files on an interactive preview of your webpage. You can navigate around your website preview using links/buttons, as you would on the live version, and edit your content inline on the page, or with the data panel or sidebar. What you see in the Visual Editor is what your website visitors will see on your live webpages.Visual Editor API
Collection
Dataset
data_config in your CloudCannon Configuration File. Datasets are used to store reusable data such as navigation links, locale strings, or site settings, and can be accessed and updated through CloudCannon's editing interfaces.Visual Editor
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 An API object is an object that exposes an API's methods, so you can work with a service or system in code. In CloudCannon, the Visual Editor API provides an API Object as its entry point: call API Object
useVersion() on window.CloudCannonAPI to get it, then use its methods to access files, Collections, and Datasets, read and write content, listen for changes, and build custom integrations in the Visual Editor.api. The following shows one way to obtain it.
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.
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.
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 A person who belongs to the same Organization as you. Team members are other CloudCannon users who have been invited to your Organization to collaborate on Sites, edit content, or manage settings. Each team member has their own CloudCannon account with individual permissions assigned through Permission Groups.change and delete events keeps your integration in sync regardless of what the Team MemberTeam Member
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.
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.
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().
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.
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().