ApiButton
An action button that loads commands from an API and executes follow-up requests with success and failure events.
import { ApiButton } from "@schukai/monster/source/components/form/api-button.mjs";Introduction
The Monster ApiButton is a command button for workflows where available actions come from an API and each action may trigger a follow-up request. Use it when a single entry point has to load and execute contextual commands such as approve, reject, retry or publish.
When to use ApiButton
- Use it for API-driven command menus: The control fetches available actions, maps labels and URLs, and renders executable buttons from the response.
- Use it when command availability depends on context: A record state, permission set or remote backend can define which actions are shown.
- Use it when success and failure handling matters: The component exposes events for loaded buttons, successful execution and failed requests.
- Do not use it for static one-click actions: A normal Button or ActionButton keeps simple flows easier to understand.
Key Features
- Dynamic action loading: Fetch command definitions and transform them into buttons via mapping selectors and templates.
- Follow-up API execution: Each generated action can trigger its own request with custom body and fetch settings.
- Lifecycle hooks and events: Use callbacks like
beforeApiand listen tomonster-button-set,monster-api-button-successfulandmonster-api-button-failed. - Consistent command surface: The command list stays visually aligned with the rest of the Monster form controls.
Typical mistakes
Avoid using ApiButton without a clear response mapping. If label, command and API targets are not mapped explicitly, the control cannot build reliable actions. Keep the command list small and task-focused; if users must browse many unrelated actions, another navigation pattern is usually better.
API Button
This example shows how the monster-api-button can load available actions from an API response and execute mocked follow-up requests.
The first request returns a title and two actions. The component maps the title to the main button label and builds the action entries from the items array. When one of the generated buttons is clicked, a second mocked request is executed and the result is shown below the control.
Javascript
import "@schukai/monster/source/components/form/api-button.mjs";
const button = document.getElementById("api-button-example");
const result = document.getElementById("api-button-example-result");
const originalFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order actions",
items: [
{ label: "Approve", api: "/mock/api-button/approve" },
{ label: "Archive", api: "/mock/api-button/archive" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (url === "/mock/api-button/approve" || url === "/mock/api-button/archive") {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
method: init.method || "GET",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("labels.button", "Actions");
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.addEventListener("monster-api-button-successful", (event) => {
result.textContent =
"Last successful action: " + (event.detail?.data?.action || "completed");
});
button.fetch("/mock/api-button/actions");<script type="module">const button = document.getElementById("api-button-example-run");
const result = document.getElementById("api-button-example-run-result");
const originalFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order actions",
items: [
{ label: "Approve", api: "/mock/api-button/approve" },
{ label: "Archive", api: "/mock/api-button/archive" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (url === "/mock/api-button/approve" || url === "/mock/api-button/archive") {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
method: init.method || "GET",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("labels.button", "Actions");
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.addEventListener("monster-api-button-successful", (event) => {
result.textContent =
"Last successful action: " + (event.detail?.data?.action || "completed");
});
button.fetch("/mock/api-button/actions");</script>HTML
<monster-api-button id="api-button-example"></monster-api-button>
<div
id="api-button-example-result"
style="margin-top: 1rem; font-size: 0.95rem; color: var(--monster-color-primary-1);"
>
Open the action list and trigger one of the mocked API calls.
</div>
<script>
const initApiButtonExample = () => {
const button = document.getElementById("api-button-example");
const result = document.getElementById("api-button-example-result");
if (!button || !result) {
return;
}
const originalFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order actions",
items: [
{ label: "Approve", api: "/mock/api-button/approve" },
{ label: "Archive", api: "/mock/api-button/archive" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (
url === "/mock/api-button/approve" ||
url === "/mock/api-button/archive"
) {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
method: init.method || "GET",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("labels.button", "Actions");
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.addEventListener("monster-api-button-successful", (event) => {
result.textContent =
"Last successful action: " +
(event.detail?.data?.action || "completed");
});
button.fetch("/mock/api-button/actions");
};
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", initApiButtonExample, {
once: true,
});
} else {
initApiButtonExample();
}
</script>Stylesheet
/** no additional stylesheet is defined **/API Workflow With Events
This example demonstrates the more advanced side of monster-api-button.
- Actions are loaded dynamically from a mocked API response.
mapping.labelSelectorturns API data into the main button label.callbacks.beforeApienriches outgoing requests before execution.api.bodyuses Monster templating to send contextual request payloads.- Success and failure events are logged to a visible activity panel.
Javascript
import "@schukai/monster/source/components/form/api-button.mjs";
const button = document.getElementById("api-button-workflow");
const log = document.getElementById("api-button-workflow-log");
const originalFetch = window.fetch.bind(window);
const addLog = (tone, title, text) => {
const colors = {
info: "var(--monster-color-primary-2)",
success: "var(--monster-color-secondary-2)",
error: "var(--monster-color-secondary-3)",
};
const entry = document.createElement("div");
entry.style.borderLeft = "4px solid " + (colors[tone] || colors.info);
entry.style.padding = "0.65rem 0.75rem";
entry.style.background = "var(--monster-bg-color-primary-1)";
entry.style.color = "var(--monster-color-primary-1)";
entry.style.borderRadius = "var(--monster-border-radius)";
entry.innerHTML =
"<strong style='display:block'>" +
title +
"</strong><span style='color:var(--monster-color-primary-1)'>" +
text +
"</span>";
log.prepend(entry);
};
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button-workflow/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order #4711",
items: [
{ label: "Approve", api: "/mock/api-button-workflow/approve" },
{ label: "Archive", api: "/mock/api-button-workflow/archive" },
{ label: "Reject", api: "/mock/api-button-workflow/reject" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (url === "/mock/api-button-workflow/reject") {
return Promise.resolve(
new Response(
JSON.stringify({
ok: false,
action: "reject",
message: "This action is blocked in the demo.",
}),
{
status: 409,
headers: { "content-type": "application/json" },
},
),
);
}
if (
url === "/mock/api-button-workflow/approve" ||
url === "/mock/api-button-workflow/archive"
) {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
body: init.body ? JSON.parse(init.body) : null,
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.setOption("api.body", {
source: "workflow-example",
command: "${key}",
target: "${label}",
context: "${value}",
});
button.setOption("callbacks.beforeApi", (fetchOptions) => {
fetchOptions.headers = Object.assign({}, fetchOptions.headers, {
"x-demo-mode": "workflow-example",
});
addLog("info", "beforeApi", "Added x-demo-mode header and templated POST body.");
});
button.addEventListener("monster-api-button-click", () => {
addLog("info", "click", "Action request started.");
});
button.addEventListener("monster-api-button-successful", (event) => {
addLog(
"success",
"success",
"Completed action: " + (event.detail?.data?.action || "unknown"),
);
});
button.addEventListener("monster-api-button-failed", (event) => {
const message =
event.detail?.error?.message ||
event.detail?.response?.statusText ||
"Request failed";
addLog("error", "failed", message);
});
button.fetch("/mock/api-button-workflow/actions");<script type="module">const button = document.getElementById("api-button-workflow-run");
const log = document.getElementById("api-button-workflow-run-log");
const originalFetch = window.fetch.bind(window);
const addLog = (tone, title, text) => {
const colors = {
info: "var(--monster-color-primary-2)",
success: "var(--monster-color-secondary-2)",
error: "var(--monster-color-secondary-3)",
};
const entry = document.createElement("div");
entry.style.borderLeft = "4px solid " + (colors[tone] || colors.info);
entry.style.padding = "0.65rem 0.75rem";
entry.style.background = "var(--monster-bg-color-primary-1)";
entry.style.color = "var(--monster-color-primary-1)";
entry.style.borderRadius = "var(--monster-border-radius)";
entry.innerHTML =
"<strong style='display:block'>" +
title +
"</strong><span style='color:var(--monster-color-primary-1)'>" +
text +
"</span>";
log.prepend(entry);
};
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button-workflow/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order #4711",
items: [
{ label: "Approve", api: "/mock/api-button-workflow/approve" },
{ label: "Archive", api: "/mock/api-button-workflow/archive" },
{ label: "Reject", api: "/mock/api-button-workflow/reject" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (url === "/mock/api-button-workflow/reject") {
return Promise.resolve(
new Response(
JSON.stringify({
ok: false,
action: "reject",
message: "This action is blocked in the demo.",
}),
{
status: 409,
headers: { "content-type": "application/json" },
},
),
);
}
if (
url === "/mock/api-button-workflow/approve" ||
url === "/mock/api-button-workflow/archive"
) {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
body: init.body ? JSON.parse(init.body) : null,
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.setOption("api.body", {
source: "workflow-example",
command: "${key}",
target: "${label}",
context: "${value}",
});
button.setOption("callbacks.beforeApi", (fetchOptions) => {
fetchOptions.headers = Object.assign({}, fetchOptions.headers, {
"x-demo-mode": "workflow-example",
});
addLog("info", "beforeApi", "Added x-demo-mode header and templated POST body.");
});
button.addEventListener("monster-api-button-click", () => {
addLog("info", "click", "Action request started.");
});
button.addEventListener("monster-api-button-successful", (event) => {
addLog(
"success",
"success",
"Completed action: " + (event.detail?.data?.action || "unknown"),
);
});
button.addEventListener("monster-api-button-failed", (event) => {
const message =
event.detail?.error?.message ||
event.detail?.response?.statusText ||
"Request failed";
addLog("error", "failed", message);
});
button.fetch("/mock/api-button-workflow/actions");</script>HTML
<monster-api-button id="api-button-workflow"></monster-api-button>
<div
style="margin-top: 1rem; display: grid; gap: 0.5rem;"
id="api-button-workflow-log"
>
<div style="color: var(--monster-color-primary-1);">
This example demonstrates dynamic action loading, <code>beforeApi</code>,
templated request bodies, and success/failure events.
</div>
</div>
<script>
const initApiButtonWorkflow = () => {
const button = document.getElementById("api-button-workflow");
const log = document.getElementById("api-button-workflow-log");
if (!button || !log) {
return;
}
const originalFetch = window.fetch.bind(window);
const addLog = (tone, title, text) => {
const colors = {
info: "var(--monster-color-primary-2)",
success: "var(--monster-color-secondary-2)",
error: "var(--monster-color-secondary-3)",
};
const entry = document.createElement("div");
entry.style.borderLeft = "4px solid " + (colors[tone] || colors.info);
entry.style.padding = "0.65rem 0.75rem";
entry.style.background = "var(--monster-bg-color-primary-1)";
entry.style.color = "var(--monster-color-primary-1)";
entry.style.borderRadius = "var(--monster-border-radius)";
entry.innerHTML =
"<strong style='display:block'>" +
title +
"</strong><span style='color:var(--monster-color-primary-1)'>" +
text +
"</span>";
log.prepend(entry);
};
window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input?.url;
if (url === "/mock/api-button-workflow/actions") {
return Promise.resolve(
new Response(
JSON.stringify({
title: "Order #4711",
items: [
{ label: "Approve", api: "/mock/api-button-workflow/approve" },
{ label: "Archive", api: "/mock/api-button-workflow/archive" },
{ label: "Reject", api: "/mock/api-button-workflow/reject" },
],
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
if (url === "/mock/api-button-workflow/reject") {
return Promise.resolve(
new Response(
JSON.stringify({
ok: false,
action: "reject",
message: "This action is blocked in the demo.",
}),
{
status: 409,
headers: { "content-type": "application/json" },
},
),
);
}
if (
url === "/mock/api-button-workflow/approve" ||
url === "/mock/api-button-workflow/archive"
) {
return Promise.resolve(
new Response(
JSON.stringify({
ok: true,
action: url.split("/").pop(),
body: init.body ? JSON.parse(init.body) : null,
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
);
}
return originalFetch(input, init);
};
button.setOption("mapping.selector", "items");
button.setOption("mapping.labelSelector", "title");
button.setOption("mapping.labelTemplate", "${path:label}");
button.setOption("mapping.apiTemplate", "${path:api}");
button.setOption("api.fetch.method", "POST");
button.setOption("api.body", {
source: "workflow-example",
command: "${key}",
target: "${label}",
context: "${value}",
});
button.setOption("callbacks.beforeApi", (fetchOptions) => {
fetchOptions.headers = Object.assign({}, fetchOptions.headers, {
"x-demo-mode": "workflow-example",
});
addLog(
"info",
"beforeApi",
"Added x-demo-mode header and templated POST body.",
);
});
button.addEventListener("monster-api-button-click", () => {
addLog("info", "click", "Action request started.");
});
button.addEventListener("monster-api-button-successful", (event) => {
addLog(
"success",
"success",
"Completed action: " + (event.detail?.data?.action || "unknown"),
);
});
button.addEventListener("monster-api-button-failed", (event) => {
const message =
event.detail?.error?.message ||
event.detail?.response?.statusText ||
"Request failed";
addLog("error", "failed", message);
});
button.fetch("/mock/api-button-workflow/actions");
};
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", initApiButtonWorkflow, {
once: true,
});
} else {
initApiButtonWorkflow();
}
</script>Stylesheet
/** no additional stylesheet is defined **/Component Design
This component is built using the Shadow DOM, which ensures that its internal structure and styles are encapsulated. By using a shadow root, the component's internal layout, logic, and behavior remain protected from external influences, such as conflicting CSS or JavaScript.
Shadow DOM and Accessibility
Shadow DOM encapsulation restricts direct access to the component's internal elements. This ensures a consistent and predictable design and behavior. However, specific styling opportunities are provided through exported parts to allow customization while maintaining encapsulation. The component's accessibility features include support for keyboard interaction and clear semantic structure for assistive technologies.
Customizing Through Exported Parts
The component exposes certain exported parts that developers can style directly using CSS. These parts allow customization of the button and related UI elements without affecting the overall structure of the component.
Available Part Attributes
container: Represents the container wrapping all button and action-related elements.button: Represents the API button itself, which can be styled for visual appearance.popper: Represents the container for any additional action elements dynamically generated.
Below is an example of how to use CSS part attributes to customize the ApiButton component.
monster-api-button::part(container) {
background-color: #f8f9fa;
padding: 8px;
border-radius: 4px;
display: inline-flex;
gap: 5px;
}
monster-api-button::part(button) {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
}
Explanation of the Example
monster-api-button::part(container): Styles the button container with padding, background, and spacing.monster-api-button::part(button): Styles the primary API button for appearance and interactivity.monster-api-button::part(button):hover: Adds a hover effect for better user feedback.monster-api-button::part(button):disabled: Adjusts the appearance of the button in a disabled state.
Accessibility
Accessibility is integrated into the component design, ensuring it can be used effectively with keyboard navigation and assistive technologies. The component manages focus states and provides accessible feedback when interacting with dynamically fetched buttons or API responses.
HTML Structure
<monster-api-button></monster-api-button>JavaScript Initialization
const element = document.createElement('monster-api-button');
document.body.appendChild(element);Exported
ApiButtonDerived from
ActionButtonOptions
The Options listed in this section are defined directly within the class. This class is derived from several parent classes, including the CustomElement class. Therefore, it inherits Options from these parent classes. If you cannot find a specific Options in this list, we recommend consulting the documentation of the ActionButton.
- since
- deprecated
Properties and Attributes
The Properties and Attributes listed in this section are defined directly within the class. This class is derived from several parent classes, including the CustomElement class and ultimately from HTMLElement. Therefore, it inherits Properties and Attributes from these parent classes. If you cannot find a specific Properties and Attributes in this list, we recommend consulting the documentation of the ActionButton.
data-monster-options: Sets the configuration options for the collapse component when used as an HTML attribute.data-monster-option-[name]: Sets the value of the configuration option[name]for the collapse component when used as an HTML attribute.
Methods
The methods listed in this section are defined directly within the class. This class is derived from several parent classes, including the CustomElement class and ultimately from HTMLElement. Therefore, it inherits methods from these parent classes. If you cannot find a specific method in this list, we recommend consulting the documentation of the ActionButton.
Behavioral methods
fetch(url)url
- {Promise}
Static methods
[instanceSymbol]()- {symbol}
instanceof operator.getCSSStyleSheet()- {CSSStyleSheet[]}
getTag()- {string}
Lifecycle methods
Lifecycle methods are called by the environment and are usually not intended to be called directly.
[assembleMethodSymbol]()- {ApiButton}
Other methods
importButtons(data)data{array|object|map|set}: data
- {ApiButton}
{Error}map is not iterable{Error}missing label configuration
Events
The component emits the following events:
monster-button-setmonster-api-button-clickmonster-api-button-successfulmonster-api-button-failed
For more information on how to handle events, see the mdn documentation.