Skip to content

Instantly share code, notes, and snippets.

@wpeasy
Last active April 1, 2025 08:42
Show Gist options
  • Save wpeasy/41eb1cd54726fe624e2d89edf5d567ae to your computer and use it in GitHub Desktop.
Save wpeasy/41eb1cd54726fe624e2d89edf5d567ae to your computer and use it in GitHub Desktop.
Bricks Ajax Loader
/*****
*
* USAGE
* On any element to use as a trigger, add the following attributes:
* - data-ajax-fetch-bricks
* - data-post-id : (int) Post ID
* - data-ajax-container : CSS selector for the root container
* - data-html-selector : Child selector inside the container to render the HTML
* Optional (children of the container):
* - data-title-selector : Selector for post.title
* - data-excerpt-selector : Selector for post.excerpt
* - data-date-selector : Selector for post.date
* - data-modified-selector : Selector for post.modified
* - data-author-selector : Selector for post.author
* Optional (grouping):
* - data-load-first=".parent-class" : Triggers the first ajax element inside each matching parent container
*/
(() => {
const triggerSelector = '[data-ajax-fetch-bricks]';
const styleId = 'wpe-bricks-dynamic-style';
const maxFetchTime = 5; // seconds
const wpe_get_by_ajax = ({ postId, containerSelector, htmlSelector, metaSelectors = {} }) => {
const { nonce, wpRestNonce } = window.bricksData || {};
if (!wpRestNonce) {
console.error("Nonce not found in window.bricksData");
return;
}
const endpoint = "/wp-json/wpe_bricks_ajax/v1/wpe_render_element";
const headers = {
"Content-Type": "application/json",
"X-WP-Nonce": wpRestNonce,
};
const body = {
postId,
nonce,
element: htmlSelector,
};
const container = document.querySelector(containerSelector);
if (!container) {
console.warn(`Container "${containerSelector}" not found`);
return;
}
container.classList.add("fetching");
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
console.warn(`Bricks fetch aborted after ${maxFetchTime}s for postId ${postId}`);
}, maxFetchTime * 1000);
fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: controller.signal,
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then(({ html, styles, post }) => {
const htmlTarget = container.querySelector(htmlSelector);
if (htmlTarget) {
const fragment = document.createRange().createContextualFragment(html);
htmlTarget.innerHTML = "";
htmlTarget.appendChild(fragment);
} else {
console.warn(`HTML target "${htmlSelector}" not found in container`);
}
if (post && typeof post === "object") {
const { title, excerpt, date, modified, author } = post;
if (metaSelectors.title) {
const el = container.querySelector(metaSelectors.title);
if (el) el.textContent = title;
}
if (metaSelectors.excerpt) {
const el = container.querySelector(metaSelectors.excerpt);
if (el) el.textContent = excerpt;
}
if (metaSelectors.date) {
const el = container.querySelector(metaSelectors.date);
if (el) el.textContent = date;
}
if (metaSelectors.modified) {
const el = container.querySelector(metaSelectors.modified);
if (el) el.textContent = modified;
}
if (metaSelectors.author) {
const el = container.querySelector(metaSelectors.author);
if (el) el.textContent = author;
}
}
if (styles) {
const parser = new DOMParser();
const doc = parser.parseFromString(styles, "text/html");
const originalStyle = doc.querySelector("style");
const cssContent = originalStyle?.textContent;
if (cssContent) {
const existingStyle = document.getElementById(styleId);
if (existingStyle) existingStyle.remove();
const styleTag = document.createElement("style");
styleTag.id = styleId;
styleTag.textContent = cssContent;
document.body.appendChild(styleTag);
}
}
})
.catch((error) => {
if (error.name === 'AbortError') {
console.warn("Fetch aborted due to timeout.");
} else {
console.error("Error rendering Bricks element:", error);
}
})
.finally(() => {
clearTimeout(timeout);
container.classList.remove("fetching");
});
};
const extractSelectors = (el) => ({
container: el.dataset.ajaxContainer,
html: el.dataset.htmlSelector,
meta: {
title: el.dataset.titleSelector,
excerpt: el.dataset.excerptSelector,
date: el.dataset.dateSelector,
modified: el.dataset.modifiedSelector,
author: el.dataset.authorSelector,
}
});
document.addEventListener("click", (event) => {
let el = event.target;
while (el && el !== document) {
if (el.matches(triggerSelector)) break;
el = el.parentElement;
}
if (el && el.matches(triggerSelector)) {
event.preventDefault();
const postId = parseInt(el.dataset.postId || "1", 10);
const { container, html, meta } = extractSelectors(el);
wpe_get_by_ajax({ postId, containerSelector: container, htmlSelector: html, metaSelectors: meta });
}
});
document.addEventListener("DOMContentLoaded", () => {
const triggers = document.querySelectorAll(`${triggerSelector}[data-load-first]`);
const processedParents = new Set();
triggers.forEach((trigger) => {
const groupSelector = trigger.getAttribute("data-load-first");
if (!groupSelector) return;
const parentGroups = document.querySelectorAll(groupSelector);
parentGroups.forEach((parent) => {
if (processedParents.has(parent)) return;
const firstChildTrigger = parent.querySelector(triggerSelector);
if (firstChildTrigger) {
const postId = parseInt(firstChildTrigger.dataset.postId || "1", 10);
const { container, html, meta } = extractSelectors(firstChildTrigger);
wpe_get_by_ajax({ postId, containerSelector: container, htmlSelector: html, metaSelectors: meta });
processedParents.add(parent);
}
});
});
});
})();
<?php
class AB_BricksAPI
{
const API_NAMESPACE = "wpe_bricks_ajax/v1";
static function init()
{
add_action("rest_api_init", [
__CLASS__,
"rest_api_init_custom_endpoints",
]);
}
static function rest_api_init_custom_endpoints()
{
register_rest_route(self::API_NAMESPACE, "wpe_render_element", [
"methods" => "POST",
"callback" => [__CLASS__, "render_content"],
"permission_callback" => [
__CLASS__,
"render_element_permissions_check",
],
]);
}
static function render_content($request)
{
$data = $request->get_json_params();
$post_id = absint($data["postId"]);
$post = get_post($post_id);
if (!$post || $post->post_status !== "publish") {
return new \WP_Error(
"bricks_api_unpublished",
__("Post not found or not published."),
["status" => 404]
);
}
$post_type = get_post_type($post_id);
$bricks_data = get_post_meta($post_id, BRICKS_DB_PAGE_CONTENT, true);
$is_bricks = is_array($bricks_data) && !empty($bricks_data);
$render_mode = $is_bricks ? "bricks" : "wordpress";
$post_data = [
"ID" => $post->ID,
"title" => get_the_title($post),
"slug" => $post->post_name,
"excerpt" => get_the_excerpt($post),
"date" => get_the_date("", $post),
"modified" => get_the_modified_date("", $post),
"permalink" => get_permalink($post),
"author" => get_the_author_meta("display_name", $post->post_author),
];
if (!$is_bricks) {
return [
"html" => apply_filters("the_content", $post->post_content),
"styles" => "",
"render_mode" => $render_mode,
"post_type" => $post_type,
"post" => $post_data,
];
}
\Bricks\Assets::$inline_css["content"] = "";
\Bricks\Assets::$css_looping_elements = [];
\Bricks\Assets::generate_css_from_elements($bricks_data, "content");
$inline_css = \Bricks\Assets::$inline_css["content"];
$inline_css .= \Bricks\Assets::generate_global_classes();
$inline_css .= \Bricks\Assets::$inline_css["global_classes"];
$css_output = "<style>{$inline_css}</style>";
return [
"html" => \Bricks\Frontend::render_data($bricks_data),
"styles" => $css_output,
"render_mode" => $render_mode,
"post_type" => $post_type,
"post" => $post_data,
];
}
static function render_element_permissions_check($request)
{
$data = $request->get_json_params();
if (
empty($data["postId"]) ||
empty($data["element"]) ||
empty($data["nonce"])
) {
return new \WP_Error(
"bricks_api_missing",
__("Missing parameters"),
["status" => 400]
);
}
return true;
}
}
// Make sure we have Bricks enabled before initialising */
add_action('after_setup_theme', function(){
if(defined('BRICKS_VERSION')){
AB_BricksAPI::init();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment