CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/hex-phoenix-html

Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements

Overview
Eval results
Files

javascript.mddocs/

JavaScript Enhancement

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.

Capabilities

Data Attribute Support

The JavaScript library automatically handles HTML data attributes to provide enhanced functionality without requiring JavaScript code changes.

Confirmation Dialogs

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 element

HTML 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:

  • Shows browser's native confirm() dialog with the specified message
  • Prevents default action if user clicks "Cancel"
  • Allows action to proceed if user clicks "OK"
  • Works with any clickable element (links, buttons, form controls)

HTTP Method Spoofing

Convert 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 security

HTML 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:

  • Creates a hidden form with the specified method and CSRF token
  • Submits the form programmatically using a button click (not form.submit())
  • Respects target attribute or opens in new window with modifier keys (Cmd/Ctrl/Shift)
  • Uses POST as the actual HTTP method, with _method parameter for method override
  • GET requests use actual GET method without CSRF token

Custom Event Handling

The library dispatches custom events that allow for additional behavior customization and integration with other JavaScript code.

phoenix.link.click Event

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
  }
});

Browser Compatibility

The library includes compatibility features for older browsers, particularly Internet Explorer 9 and earlier.

CustomEvent Polyfill

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:

  • Modern browsers: Native CustomEvent
  • Internet Explorer 9+: Polyfilled CustomEvent
  • Internet Explorer 8 and below: Not supported

Integration Examples

Complete Phoenix Template Integration

# 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>

Advanced Custom Handling

// 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));
    }
  });
});

Phoenix LiveView Integration

// 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...
  });
});

Installation and Setup

Manual Installation

<!-- 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>

Build Tool Integration

// 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';

Content Security Policy (CSP)

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';

Security Considerations

CSRF Protection

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()

XSS Prevention

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-escaped

URL Validation

Always 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

Troubleshooting

Common Issues

// 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

Debug Mode

// 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')
  });
});

Best Practices

  1. CSRF Tokens: Always include valid CSRF tokens for non-GET requests
  2. URL Safety: Use server-generated URLs, never user input directly
  3. Confirmation Messages: Use clear, specific confirmation text
  4. Loading States: Add visual feedback for method requests
  5. Graceful Degradation: Ensure functionality works without JavaScript when possible
  6. Event Cleanup: Remove event listeners when appropriate for SPA-style applications
  7. Testing: Test with JavaScript disabled to ensure core functionality remains accessible

Install with Tessl CLI

npx tessl i tessl/hex-phoenix-html

docs

forms.md

html-safety.md

index.md

javascript.md

tile.json