Packages
phoenix_duskmoon
9.5.3
9.13.0
9.12.3
9.12.2
9.12.1
9.12.0
9.11.0
9.10.1
9.10.0
9.9.10
9.9.9
9.9.8
9.9.7
9.9.6
9.9.4
9.9.3
9.9.2
9.9.1
9.9.0
9.8.0
9.7.1
9.7.0
9.6.4
9.6.3
9.6.2
9.6.1
9.6.0
9.5.4
9.5.3
9.5.2
9.5.1
9.5.0
9.4.3
9.4.2
9.4.1
9.4.0
9.3.0
9.2.0
9.1.4
9.1.3
9.1.1
9.1.1-rc.1
retired
9.0.1
9.0.0-rc.3
9.0.0-rc.2
9.0.0-rc.1
8.0.0
7.2.1
7.2.0
7.1.3
7.1.2
7.1.1
6.3.2
6.3.1
6.3.0
6.2.0
6.1.3
6.1.2
6.1.1
6.1.0
6.0.10
6.0.9
6.0.8
6.0.7
6.0.6
6.0.5
6.0.4
6.0.3
6.0.2
6.0.1
6.0.0
5.2.0-beta.5
5.2.0-beta.4
5.2.0-beta.3
5.2.0-beta.2
5.2.0-beta.1
5.1.0
5.0.6
5.0.5
5.0.4
5.0.3
5.0.2
5.0.1
5.0.0
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.2
4.3.1
4.3.0
4.2.0
4.1.1
4.1.0
4.0.0
Duskmoon UI component library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
assets/js/phoenix_duskmoon.js
/**
* Phoenix Duskmoon UI v9 - Web Component Integration
*
* Provides LiveView hooks for bridging Phoenix LiveView events
* with duskmoon-elements custom elements.
*/
/** Standard duskmoon-elements event names for automatic forwarding */
const DM_EVENTS = [
"dm-click", "dm-change", "dm-input", "dm-focus", "dm-blur",
"dm-submit", "dm-select", "dm-close", "dm-open", "dm-toggle",
];
/**
* WebComponentHook - Universal LiveView ↔ Custom Element bridge
*
* This hook enables seamless communication between Phoenix LiveView
* and duskmoon-elements custom elements (el-dm-*).
*
* ## Event Bridging Patterns
*
* ### Forward custom element events to LiveView:
* Use `duskmoon-send-{event}` attribute:
* ```html
* <el-dm-button duskmoon-send-click="handle_click">Click me</el-dm-button>
* ```
*
* ### Receive LiveView events in custom element:
* Use `duskmoon-receive-{event}` attribute:
* ```html
* <el-dm-input duskmoon-receive-reset="reset">Input</el-dm-input>
* ```
*
* ### Automatic dm-* event forwarding:
* Events prefixed with `dm-` (dm-click, dm-change, dm-input, etc.)
* are automatically forwarded to LiveView when corresponding
* `phx-{event}` attribute exists.
*
* ## Usage in Phoenix templates:
* ```heex
* <el-dm-button
* phx-hook="WebComponentHook"
* phx-click="button_clicked"
* variant="primary"
* >
* Click me
* </el-dm-button>
* ```
*/
export const WebComponentHook = {
mounted() {
this._sendListeners = [];
this._setupEventBridging();
this._setupAutomaticForwarding();
},
updated() {
// Re-check attributes on update in case they changed
this._setupAutomaticForwarding();
},
destroyed() {
// Remove duskmoon-send-* event listeners
if (this._sendListeners) {
this._sendListeners.forEach(({ event, listener }) => {
this.el.removeEventListener(event, listener);
});
this._sendListeners = null;
}
// Remove dm-* automatic forwarding listeners
DM_EVENTS.forEach((dmEvent) => {
const listenerKey = `_dm_listener_${dmEvent}`;
if (this[listenerKey]) {
this.el.removeEventListener(dmEvent, this[listenerKey]);
this[listenerKey] = null;
}
});
},
/**
* Setup explicit event bridging via duskmoon-send-* and duskmoon-receive-* attributes
*/
_setupEventBridging() {
const attrs = this.el.attributes;
const phxTarget = this.el.getAttribute("phx-target");
// Helper to push events with optional target
const pushEvent = (event, payload, callback) => {
if (phxTarget) {
this.pushEventTo(phxTarget, event, payload, callback);
} else {
this.pushEvent(event, payload, callback);
}
};
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
// Handle duskmoon-send-{event}="{phxEvent};{optionalCallback}"
if (/^duskmoon-send-/.test(attr.name)) {
const eventName = attr.name.replace(/^duskmoon-send-/, "");
const [phxEvent, callbackName] = attr.value.split(";");
const listener = ({ detail }) => {
pushEvent(phxEvent, detail || {}, (response) => {
if (callbackName && typeof this[callbackName] === "function") {
this[callbackName](response, detail, eventName);
}
});
};
this.el.addEventListener(eventName, listener);
this._sendListeners.push({ event: eventName, listener });
}
// Handle duskmoon-receive-{event}="{handlerMethodOrEmpty}"
if (/^duskmoon-receive-/.test(attr.name)) {
const eventName = attr.name.replace(/^duskmoon-receive-/, "");
const handler = attr.value;
this.handleEvent(eventName, (payload) => {
if (handler && typeof this.el[handler] === "function") {
this.el[handler](payload);
} else {
// Dispatch as custom event on the element
this.el.dispatchEvent(
new CustomEvent(eventName, {
detail: payload,
bubbles: true,
composed: true,
})
);
}
});
}
// Handle duskmoon-receive="{phxEvent};{callbackName}"
if (attr.name === "duskmoon-receive") {
const [phxEvent, callbackName] = attr.value.split(";");
this.handleEvent(phxEvent, (payload) => {
if (typeof this.el[callbackName] === "function") {
this.el[callbackName](payload);
}
});
}
}
},
/**
* Setup automatic forwarding for dm-* events to phx-* handlers
*
* This enables the common pattern of:
* <el-dm-button phx-hook="WebComponentHook" phx-click="handle_click">
*
* When el-dm-button emits 'dm-click', it automatically calls the phx-click handler
*/
_setupAutomaticForwarding() {
const phxTarget = this.el.getAttribute("phx-target");
DM_EVENTS.forEach((dmEvent) => {
// Map dm-click -> phx-click, dm-change -> phx-change, etc.
const phxAttr = "phx-" + dmEvent.replace("dm-", "");
const phxEvent = this.el.getAttribute(phxAttr);
if (phxEvent) {
// Remove existing listener to avoid duplicates on update
const listenerKey = `_dm_listener_${dmEvent}`;
if (this[listenerKey]) {
this.el.removeEventListener(dmEvent, this[listenerKey]);
}
// Create and store the listener
this[listenerKey] = (e) => {
const detail = e.detail || {};
// Include common form data if available
const payload = {
...detail,
value: this.el.value,
name: this.el.name,
checked: this.el.checked,
};
if (phxTarget) {
this.pushEventTo(phxTarget, phxEvent, payload);
} else {
this.pushEvent(phxEvent, payload);
}
};
this.el.addEventListener(dmEvent, this[listenerKey]);
}
});
},
};
/**
* FormElementHook - Specialized hook for form custom elements
*
* Extends WebComponentHook with form-specific behavior:
* - Integrates with Phoenix.HTML.Form
* - Handles phx-feedback-for error display
* - Manages form field state
*/
export const FormElementHook = {
...WebComponentHook,
mounted() {
// Call parent mounted
WebComponentHook.mounted.call(this);
// Setup form integration
this._setupFormIntegration();
},
_setupFormIntegration() {
const name = this.el.getAttribute("name");
const feedbackFor = this.el.getAttribute("phx-feedback-for") || name;
if (feedbackFor) {
// Watch for phx-no-feedback class changes to show/hide errors
this._setupFeedbackObserver(feedbackFor);
}
},
destroyed() {
WebComponentHook.destroyed.call(this);
if (this._feedbackObserver) {
this._feedbackObserver.disconnect();
this._feedbackObserver = null;
}
},
_setupFeedbackObserver(feedbackFor) {
// Find the form wrapper that Phoenix LiveView manages
const form = this.el.closest("form");
if (!form) return;
// Observe class changes on form for feedback state
this._feedbackObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
const hasNoFeedback = form.classList.contains("phx-no-feedback");
// Dispatch event to custom element to show/hide errors
this.el.dispatchEvent(
new CustomEvent("dm-feedback-change", {
detail: { showFeedback: !hasNoFeedback, field: feedbackFor },
})
);
}
});
});
this._feedbackObserver.observe(form, { attributes: true, attributeFilter: ["class"] });
},
};
// Export to window for non-module usage
if (typeof window !== "undefined") {
window.__WebComponentHook__ = WebComponentHook;
window.__FormElementHook__ = FormElementHook;
}