assets/js/controllers/outlet-button-controller.js

import { Controller } from "@hotwired/stimulus"

/**
 * OutletButton Stimulus Controller
 *
 * Manages inter-controller communication through data passing and event dispatching.
 * Provides a button component that can collect data from other controllers and
 * dispatch custom events for complex workflow coordination.
 *
 * Values:
 * - btnData: String - JSON payload in data-outlet-btn-btn-data-value (parsed safely)
 * - requireData: Boolean - Whether data is required for button activation
 */
class OutletButton extends Controller {
    static values = {
        btnData: String,
        requireData: Boolean,
    }

    /**
     * @param {string|null|undefined} raw Attribute value from data-outlet-btn-btn-data-value
     * @return {Record<string, unknown>}
     */
    parseBtnData(raw) {
        if (raw === null || raw === undefined) {
            return {};
        }

        const trimmed = String(raw).trim();
        if (trimmed === "" || trimmed === "{}") {
            return {};
        }

        try {
            const parsed = JSON.parse(trimmed);
            if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
                return parsed;
            }
        } catch (error) {
            if (trimmed !== "{") {
                console.warn("outlet-btn: invalid btn-data-value JSON", trimmed, error);
            }
        }

        return {};
    }

    /**
     * @return {Record<string, unknown>}
     */
    getBtnData() {
        return this.parseBtnData(this.btnDataValue);
    }

    /**
     * Handle button data value changes
     * Updates button state based on data availability and requirements
     */
    btnDataValueChanged() {
        if (this.btnDataValue === null || this.btnDataValue === undefined) {
            this.btnDataValue = "";
        }

        const data = this.getBtnData();
        if (this.requireDataValue && Object.keys(data).length === 0) {
            this.element.disabled = true;
        } else {
            this.element.disabled = false;
        }
    }

    /**
     * Add data to button for communication
     *
     * @param {Record<string, unknown>} data - Data object to associate with button
     */
    addBtnData(data) {
        this.btnDataValue = JSON.stringify(data ?? {});
    }

    /**
     * Fire custom event with button data
     *
     * @param {Event} event - Click event from button
     */
    fireNotice(event) {
        this.dispatch("outlet-button-clicked", { detail: this.getBtnData() });
    }

    /**
     * @param {Function} callback - Event handler function
     */
    addListener(callback) {
        this.element.addEventListener("outlet-btn:outlet-button-clicked", callback);
    }

    /**
     * @param {Function} callback - Event handler function to remove
     */
    removeListener(callback) {
        this.element.removeEventListener("outlet-btn:outlet-button-clicked", callback);
    }
}
if (!window.Controllers) {
    window.Controllers = {};
}
window.Controllers["outlet-btn"] = OutletButton;