ApiButton

An action button that loads commands from an API and executes follow-up requests with success and failure events.

Import
the javascript logo
import { ApiButton } from "@schukai/monster/source/components/form/api-button.mjs";
Source
the git logo
Package
the npm logo
Since
3.32.0

This showcase loads actions dynamically, adds request metadata in beforeApi, and logs both success and failure events.

Event log

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 beforeApi and listen to monster-button-set, monster-api-button-successful and monster-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.

Open the action list and trigger one of the mocked API calls.

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 **/
Open in playground

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.labelSelector turns API data into the main button label.
  • callbacks.beforeApi enriches outgoing requests before execution.
  • api.body uses Monster templating to send contextual request payloads.
  • Success and failure events are logged to a visible activity panel.
This example demonstrates dynamic action loading, beforeApi, templated request bodies, and success/failure events.

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 **/
Open in playground

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

ApiButton

Derived from

ActionButton

Options

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.

Option
Type
Default
Description
mapping
object
undefined
The mapping object.
mapping.selector
string
undefined
The selector to find the buttons in the response.
mapping.labelSelector
string
undefined
The selector to find the label for the button.
mapping.labelTemplate
string
undefined
The template to create the label for the button.
mapping.apiTemplate
string
undefined
The template to create the api for the button.
mapping.urlTemplate
string
undefined
The template to create the url for the button.
mapping.filter
function
undefined
The filter function to filter the buttons.
url
string
undefined
The url to fetch the data.
api
object
undefined
The api options.
api.fetch
object
undefined
The fetch options.
api.body
string
undefined
The body template.
callbacks
object
undefined
The callbacks object.
callbacks.beforeApi
function
undefined
The beforeApi callback.
fetch
object
undefined
The fetch options.
fetch.redirect
string
undefined
The redirect option.
fetch.method
string
undefined
The method option.
fetch.mode
string
undefined
The mode option.
fetch.credentials
string
undefined
The credentials option.
fetch.headers
object
undefined
The headers option.
fetch.headers.accept
string
undefined
The acceptance option.

  • 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)
Parameters
  • url
Returns
  • {Promise}

Static methods

[instanceSymbol]()
Returns
  • {symbol}
This method is called by the instanceof operator.
getCSSStyleSheet()
Returns
  • {CSSStyleSheet[]}
getTag()
Returns
  • {string}

Lifecycle methods

Lifecycle methods are called by the environment and are usually not intended to be called directly.

[assembleMethodSymbol]()
Returns
  • {ApiButton}

Other methods

importButtons(data)
Parameters
  • data {array|object|map|set}: data
Returns
  • {ApiButton}
Throws
  • {Error} map is not iterable
  • {Error} missing label configuration
Import buttons from a map.

Events

The component emits the following events:

  • monster-button-set
  • monster-api-button-click
  • monster-api-button-successful
  • monster-api-button-failed

For more information on how to handle events, see the mdn documentation.

The current width of the area is too small to display the content correctly.