Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements
Phoenix.HTML includes a lightweight JavaScript library that provides progressive enhancement for links and forms through data attributes. The library supports confirmation dialogs, HTTP method spoofing, and custom event handling while maintaining compatibility with older browsers.
The JavaScript library automatically handles HTML data attributes to provide enhanced functionality without requiring JavaScript code changes.
Automatically show confirmation dialogs for destructive actions using the data-confirm attribute.
// Automatic handling - no JavaScript code required
// Add data-confirm attribute to any clickable elementHTML Usage Examples:
<!-- Basic confirmation -->
<a href="/users/123" data-method="delete" data-confirm="Are you sure?">Delete User</a>
<!-- Form submission with confirmation -->
<button type="submit" data-confirm="This will permanently delete all data. Continue?">
Delete All Data
</button>
<!-- Link with confirmation -->
<a href="/logout" data-confirm="Are you sure you want to log out?">Logout</a>Behavior:
confirm() dialog with the specified messageConvert links into HTTP requests with methods other than GET using data-method attribute.
// Required attributes for method spoofing:
// data-method: "patch|post|put|delete" - HTTP method to use
// data-to: "url" - Target URL for the request
// data-csrf: "token" - CSRF token for securityHTML Usage Examples:
<!-- DELETE request via link -->
<a href="#"
data-method="delete"
data-to="/users/123"
data-csrf="<%= csrf_token %>">
Delete User
</a>
<!-- PUT request for state changes -->
<a href="#"
data-method="put"
data-to="/posts/456/publish"
data-csrf="<%= csrf_token %>">
Publish Post
</a>
<!-- PATCH request with confirmation -->
<a href="#"
data-method="patch"
data-to="/users/123/activate"
data-csrf="<%= csrf_token %>"
data-confirm="Activate this user account?">
Activate User
</a>
<!-- With target specification -->
<a href="#"
data-method="post"
data-to="/reports/generate"
data-csrf="<%= csrf_token %>"
target="_blank">
Generate Report
</a>Generated Form Structure:
<!-- The library creates a hidden form like this: -->
<form method="post" action="/users/123" style="display: none;" target="_blank">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="_csrf_token" value="token_value">
<input type="submit">
</form>Behavior:
form.submit())target attribute or opens in new window with modifier keys (Cmd/Ctrl/Shift)_method parameter for method overrideThe library dispatches custom events that allow for additional behavior customization and integration with other JavaScript code.
Custom event fired before processing data-method and data-confirm attributes, allowing for custom behavior modification.
// Event: phoenix.link.click
// Type: CustomEvent with bubbles: true, cancelable: true
// Target: The clicked element
// Timing: Fired before data-method and data-confirm processing
window.addEventListener('phoenix.link.click', function(e) {
// e.target - the clicked element
// e.preventDefault() - prevent default Phoenix.HTML handling
// e.stopPropagation() - prevent data-confirm processing
// return false - disable data-method processing
});Custom Event Examples:
// Add custom prompt behavior
window.addEventListener('phoenix.link.click', function(e) {
var message = e.target.getAttribute("data-prompt");
var answer = e.target.getAttribute("data-prompt-answer");
if (message && answer && (answer !== window.prompt(message))) {
e.preventDefault(); // Cancel the action
}
});
// Add analytics tracking
window.addEventListener('phoenix.link.click', function(e) {
var action = e.target.getAttribute("data-method");
var url = e.target.getAttribute("data-to");
if (action && url) {
analytics.track('method_link_click', {
method: action,
url: url,
element: e.target.tagName.toLowerCase()
});
}
});
// Custom loading states
window.addEventListener('phoenix.link.click', function(e) {
var target = e.target;
if (target.getAttribute('data-method')) {
target.classList.add('loading');
target.setAttribute('disabled', 'disabled');
// Note: Phoenix.HTML will proceed with form submission
// You may need additional handling for cleanup
}
});
// Disable data-method for specific conditions
window.addEventListener('phoenix.link.click', function(e) {
var target = e.target;
if (target.classList.contains('disabled') || target.hasAttribute('disabled')) {
return false; // Disables data-method processing
}
});The library includes compatibility features for older browsers, particularly Internet Explorer 9 and earlier.
Provides CustomEvent support for browsers that don't have native implementation.
// Internal polyfill - automatically used when needed
// Creates CustomEvent constructor for IE<=9
function CustomEvent(event, params) {
params = params || {bubbles: false, cancelable: false, detail: undefined};
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}Supported Browsers:
# In your Phoenix template (.heex file)
<div class="user-actions">
<%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %>
<%= link "Delete",
to: "#",
"data-method": "delete",
"data-to": Routes.user_path(@conn, :delete, @user),
"data-csrf": Plug.CSRFProtection.get_csrf_token(),
"data-confirm": "Are you sure you want to delete #{@user.name}?",
class: "btn btn-danger" %>
<%= link "Archive",
to: "#",
"data-method": "patch",
"data-to": Routes.user_path(@conn, :update, @user, action: :archive),
"data-csrf": Plug.CSRFProtection.get_csrf_token(),
"data-confirm": "Archive this user?",
class: "btn btn-warning" %>
</div>
<!-- Include the Phoenix HTML JavaScript -->
<script src="<%= Routes.static_path(@conn, "/js/phoenix_html.js") %>"></script>// Comprehensive custom event handler
document.addEventListener('DOMContentLoaded', function() {
// Track all Phoenix.HTML link interactions
window.addEventListener('phoenix.link.click', function(e) {
var target = e.target;
var method = target.getAttribute('data-method');
var url = target.getAttribute('data-to');
var confirm = target.getAttribute('data-confirm');
// Custom analytics
if (method && url) {
analytics.track('phoenix_link_action', {
method: method,
path: new URL(url, window.location.origin).pathname,
confirmed: !confirm || window.confirm ? true : false
});
}
// Custom loading states
if (method && !target.classList.contains('no-loading')) {
target.classList.add('loading');
target.innerHTML = '<span class="spinner"></span> ' + target.innerHTML;
}
// Custom confirmation for sensitive operations
if (target.classList.contains('super-dangerous')) {
var userInput = window.prompt(
'Type "DELETE" to confirm this irreversible action:'
);
if (userInput !== 'DELETE') {
e.preventDefault();
return false;
}
}
});
// Handle custom data attributes
document.addEventListener('click', function(e) {
var target = e.target;
// Custom data-disable-for attribute
var disableFor = target.getAttribute('data-disable-for');
if (disableFor) {
target.disabled = true;
setTimeout(function() {
target.disabled = false;
}, parseInt(disableFor, 10));
}
});
});// Integration with Phoenix LiveView
document.addEventListener('DOMContentLoaded', function() {
// Re-initialize Phoenix.HTML behavior after LiveView updates
document.addEventListener('phx:update', function() {
// Phoenix.HTML automatically handles new elements
// No additional initialization needed
});
// Custom handling for LiveView-specific patterns
window.addEventListener('phoenix.link.click', function(e) {
var target = e.target;
// Don't interfere with LiveView elements
if (target.hasAttribute('phx-click') || target.closest('[phx-click]')) {
// Let LiveView handle the event
return;
}
// Standard Phoenix.HTML processing continues...
});
});<!-- Include in your layout template -->
<script src="/js/phoenix_html.js"></script>
<!-- Or from CDN (replace VERSION) -->
<script src="https://unpkg.com/phoenix_html@VERSION/priv/static/phoenix_html.js"></script>// With webpack, vite, or similar
import "phoenix_html"
// Or CommonJS
require("phoenix_html")
// Or ES6 import (if configured)
import 'phoenix_html/priv/static/phoenix_html.js';The library uses eval() equivalent operations for form creation. Update CSP headers if needed:
Content-Security-Policy: script-src 'self' 'unsafe-eval';Or more specifically:
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval';Always include CSRF tokens with data-method requests:
# In Phoenix templates
"data-csrf": Plug.CSRFProtection.get_csrf_token()
# In Phoenix controllers
csrf_token = Plug.CSRFProtection.get_csrf_token()The library doesn't modify user content but relies on Phoenix.HTML's server-side escaping:
# Safe - content is escaped by Phoenix.HTML
<%= link "Delete #{@user.name}",
to: "#",
"data-confirm": "Delete #{@user.name}?",
"data-method": "delete" %>
# Dangerous - if @user.name contains HTML
# Use html_escape/1 or ensure data is pre-escapedAlways validate data-to URLs on the server side:
# Good - relative URLs
"data-to": Routes.user_path(@conn, :delete, @user)
# Dangerous - user-controlled URLs
"data-to": @user_provided_url # Could be malicious// Issue: Events not firing
// Solution: Ensure library is loaded before DOM ready
// Issue: CSRF token errors
// Solution: Verify token is current and matches server expectations
// Issue: Method not working
// Solution: Check that all required attributes are present
// - data-method
// - data-to
// - data-csrf (for non-GET requests)
// Issue: Confirmation not showing
// Solution: Check for JavaScript errors and event propagation stopping
// Issue: Form submission not working
// Solution: Verify server accepts _method parameter for method override// Add debug logging
window.addEventListener('phoenix.link.click', function(e) {
console.log('Phoenix.HTML link clicked:', {
target: e.target,
method: e.target.getAttribute('data-method'),
to: e.target.getAttribute('data-to'),
confirm: e.target.getAttribute('data-confirm'),
csrf: e.target.getAttribute('data-csrf')
});
});Install with Tessl CLI
npx tessl i tessl/hex-phoenix-html