0
# JavaScript Enhancement
1
2
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.
3
4
## Capabilities
5
6
### Data Attribute Support
7
8
The JavaScript library automatically handles HTML data attributes to provide enhanced functionality without requiring JavaScript code changes.
9
10
#### Confirmation Dialogs
11
12
Automatically show confirmation dialogs for destructive actions using the `data-confirm` attribute.
13
14
```javascript { .api }
15
// Automatic handling - no JavaScript code required
16
// Add data-confirm attribute to any clickable element
17
```
18
19
**HTML Usage Examples:**
20
21
```html
22
<!-- Basic confirmation -->
23
<a href="/users/123" data-method="delete" data-confirm="Are you sure?">Delete User</a>
24
25
<!-- Form submission with confirmation -->
26
<button type="submit" data-confirm="This will permanently delete all data. Continue?">
27
Delete All Data
28
</button>
29
30
<!-- Link with confirmation -->
31
<a href="/logout" data-confirm="Are you sure you want to log out?">Logout</a>
32
```
33
34
**Behavior:**
35
- Shows browser's native `confirm()` dialog with the specified message
36
- Prevents default action if user clicks "Cancel"
37
- Allows action to proceed if user clicks "OK"
38
- Works with any clickable element (links, buttons, form controls)
39
40
#### HTTP Method Spoofing
41
42
Convert links into HTTP requests with methods other than GET using `data-method` attribute.
43
44
```javascript { .api }
45
// Required attributes for method spoofing:
46
// data-method: "patch|post|put|delete" - HTTP method to use
47
// data-to: "url" - Target URL for the request
48
// data-csrf: "token" - CSRF token for security
49
```
50
51
**HTML Usage Examples:**
52
53
```html
54
<!-- DELETE request via link -->
55
<a href="#"
56
data-method="delete"
57
data-to="/users/123"
58
data-csrf="<%= csrf_token %>">
59
Delete User
60
</a>
61
62
<!-- PUT request for state changes -->
63
<a href="#"
64
data-method="put"
65
data-to="/posts/456/publish"
66
data-csrf="<%= csrf_token %>">
67
Publish Post
68
</a>
69
70
<!-- PATCH request with confirmation -->
71
<a href="#"
72
data-method="patch"
73
data-to="/users/123/activate"
74
data-csrf="<%= csrf_token %>"
75
data-confirm="Activate this user account?">
76
Activate User
77
</a>
78
79
<!-- With target specification -->
80
<a href="#"
81
data-method="post"
82
data-to="/reports/generate"
83
data-csrf="<%= csrf_token %>"
84
target="_blank">
85
Generate Report
86
</a>
87
```
88
89
**Generated Form Structure:**
90
91
```html
92
<!-- The library creates a hidden form like this: -->
93
<form method="post" action="/users/123" style="display: none;" target="_blank">
94
<input type="hidden" name="_method" value="delete">
95
<input type="hidden" name="_csrf_token" value="token_value">
96
<input type="submit">
97
</form>
98
```
99
100
**Behavior:**
101
- Creates a hidden form with the specified method and CSRF token
102
- Submits the form programmatically using a button click (not `form.submit()`)
103
- Respects `target` attribute or opens in new window with modifier keys (Cmd/Ctrl/Shift)
104
- Uses POST as the actual HTTP method, with `_method` parameter for method override
105
- GET requests use actual GET method without CSRF token
106
107
### Custom Event Handling
108
109
The library dispatches custom events that allow for additional behavior customization and integration with other JavaScript code.
110
111
#### phoenix.link.click Event
112
113
Custom event fired before processing `data-method` and `data-confirm` attributes, allowing for custom behavior modification.
114
115
```javascript { .api }
116
// Event: phoenix.link.click
117
// Type: CustomEvent with bubbles: true, cancelable: true
118
// Target: The clicked element
119
// Timing: Fired before data-method and data-confirm processing
120
121
window.addEventListener('phoenix.link.click', function(e) {
122
// e.target - the clicked element
123
// e.preventDefault() - prevent default Phoenix.HTML handling
124
// e.stopPropagation() - prevent data-confirm processing
125
// return false - disable data-method processing
126
});
127
```
128
129
**Custom Event Examples:**
130
131
```javascript
132
// Add custom prompt behavior
133
window.addEventListener('phoenix.link.click', function(e) {
134
var message = e.target.getAttribute("data-prompt");
135
var answer = e.target.getAttribute("data-prompt-answer");
136
137
if (message && answer && (answer !== window.prompt(message))) {
138
e.preventDefault(); // Cancel the action
139
}
140
});
141
142
// Add analytics tracking
143
window.addEventListener('phoenix.link.click', function(e) {
144
var action = e.target.getAttribute("data-method");
145
var url = e.target.getAttribute("data-to");
146
147
if (action && url) {
148
analytics.track('method_link_click', {
149
method: action,
150
url: url,
151
element: e.target.tagName.toLowerCase()
152
});
153
}
154
});
155
156
// Custom loading states
157
window.addEventListener('phoenix.link.click', function(e) {
158
var target = e.target;
159
160
if (target.getAttribute('data-method')) {
161
target.classList.add('loading');
162
target.setAttribute('disabled', 'disabled');
163
164
// Note: Phoenix.HTML will proceed with form submission
165
// You may need additional handling for cleanup
166
}
167
});
168
169
// Disable data-method for specific conditions
170
window.addEventListener('phoenix.link.click', function(e) {
171
var target = e.target;
172
173
if (target.classList.contains('disabled') || target.hasAttribute('disabled')) {
174
return false; // Disables data-method processing
175
}
176
});
177
```
178
179
### Browser Compatibility
180
181
The library includes compatibility features for older browsers, particularly Internet Explorer 9 and earlier.
182
183
#### CustomEvent Polyfill
184
185
Provides CustomEvent support for browsers that don't have native implementation.
186
187
```javascript { .api }
188
// Internal polyfill - automatically used when needed
189
// Creates CustomEvent constructor for IE<=9
190
function CustomEvent(event, params) {
191
params = params || {bubbles: false, cancelable: false, detail: undefined};
192
var evt = document.createEvent('CustomEvent');
193
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
194
return evt;
195
}
196
```
197
198
**Supported Browsers:**
199
- Modern browsers: Native CustomEvent
200
- Internet Explorer 9+: Polyfilled CustomEvent
201
- Internet Explorer 8 and below: Not supported
202
203
### Integration Examples
204
205
#### Complete Phoenix Template Integration
206
207
```elixir
208
# In your Phoenix template (.heex file)
209
<div class="user-actions">
210
<%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %>
211
212
<%= link "Delete",
213
to: "#",
214
"data-method": "delete",
215
"data-to": Routes.user_path(@conn, :delete, @user),
216
"data-csrf": Plug.CSRFProtection.get_csrf_token(),
217
"data-confirm": "Are you sure you want to delete #{@user.name}?",
218
class: "btn btn-danger" %>
219
220
<%= link "Archive",
221
to: "#",
222
"data-method": "patch",
223
"data-to": Routes.user_path(@conn, :update, @user, action: :archive),
224
"data-csrf": Plug.CSRFProtection.get_csrf_token(),
225
"data-confirm": "Archive this user?",
226
class: "btn btn-warning" %>
227
</div>
228
229
<!-- Include the Phoenix HTML JavaScript -->
230
<script src="<%= Routes.static_path(@conn, "/js/phoenix_html.js") %>"></script>
231
```
232
233
#### Advanced Custom Handling
234
235
```javascript
236
// Comprehensive custom event handler
237
document.addEventListener('DOMContentLoaded', function() {
238
// Track all Phoenix.HTML link interactions
239
window.addEventListener('phoenix.link.click', function(e) {
240
var target = e.target;
241
var method = target.getAttribute('data-method');
242
var url = target.getAttribute('data-to');
243
var confirm = target.getAttribute('data-confirm');
244
245
// Custom analytics
246
if (method && url) {
247
analytics.track('phoenix_link_action', {
248
method: method,
249
path: new URL(url, window.location.origin).pathname,
250
confirmed: !confirm || window.confirm ? true : false
251
});
252
}
253
254
// Custom loading states
255
if (method && !target.classList.contains('no-loading')) {
256
target.classList.add('loading');
257
target.innerHTML = '<span class="spinner"></span> ' + target.innerHTML;
258
}
259
260
// Custom confirmation for sensitive operations
261
if (target.classList.contains('super-dangerous')) {
262
var userInput = window.prompt(
263
'Type "DELETE" to confirm this irreversible action:'
264
);
265
266
if (userInput !== 'DELETE') {
267
e.preventDefault();
268
return false;
269
}
270
}
271
});
272
273
// Handle custom data attributes
274
document.addEventListener('click', function(e) {
275
var target = e.target;
276
277
// Custom data-disable-for attribute
278
var disableFor = target.getAttribute('data-disable-for');
279
if (disableFor) {
280
target.disabled = true;
281
setTimeout(function() {
282
target.disabled = false;
283
}, parseInt(disableFor, 10));
284
}
285
});
286
});
287
```
288
289
#### Phoenix LiveView Integration
290
291
```javascript
292
// Integration with Phoenix LiveView
293
document.addEventListener('DOMContentLoaded', function() {
294
// Re-initialize Phoenix.HTML behavior after LiveView updates
295
document.addEventListener('phx:update', function() {
296
// Phoenix.HTML automatically handles new elements
297
// No additional initialization needed
298
});
299
300
// Custom handling for LiveView-specific patterns
301
window.addEventListener('phoenix.link.click', function(e) {
302
var target = e.target;
303
304
// Don't interfere with LiveView elements
305
if (target.hasAttribute('phx-click') || target.closest('[phx-click]')) {
306
// Let LiveView handle the event
307
return;
308
}
309
310
// Standard Phoenix.HTML processing continues...
311
});
312
});
313
```
314
315
## Installation and Setup
316
317
### Manual Installation
318
319
```html
320
<!-- Include in your layout template -->
321
<script src="/js/phoenix_html.js"></script>
322
323
<!-- Or from CDN (replace VERSION) -->
324
<script src="https://unpkg.com/phoenix_html@VERSION/priv/static/phoenix_html.js"></script>
325
```
326
327
### Build Tool Integration
328
329
```javascript
330
// With webpack, vite, or similar
331
import "phoenix_html"
332
333
// Or CommonJS
334
require("phoenix_html")
335
336
// Or ES6 import (if configured)
337
import 'phoenix_html/priv/static/phoenix_html.js';
338
```
339
340
### Content Security Policy (CSP)
341
342
The library uses `eval()` equivalent operations for form creation. Update CSP headers if needed:
343
344
```
345
Content-Security-Policy: script-src 'self' 'unsafe-eval';
346
```
347
348
Or more specifically:
349
```
350
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval';
351
```
352
353
## Security Considerations
354
355
### CSRF Protection
356
357
Always include CSRF tokens with `data-method` requests:
358
359
```elixir
360
# In Phoenix templates
361
"data-csrf": Plug.CSRFProtection.get_csrf_token()
362
363
# In Phoenix controllers
364
csrf_token = Plug.CSRFProtection.get_csrf_token()
365
```
366
367
### XSS Prevention
368
369
The library doesn't modify user content but relies on Phoenix.HTML's server-side escaping:
370
371
```elixir
372
# Safe - content is escaped by Phoenix.HTML
373
<%= link "Delete #{@user.name}",
374
to: "#",
375
"data-confirm": "Delete #{@user.name}?",
376
"data-method": "delete" %>
377
378
# Dangerous - if @user.name contains HTML
379
# Use html_escape/1 or ensure data is pre-escaped
380
```
381
382
### URL Validation
383
384
Always validate `data-to` URLs on the server side:
385
386
```elixir
387
# Good - relative URLs
388
"data-to": Routes.user_path(@conn, :delete, @user)
389
390
# Dangerous - user-controlled URLs
391
"data-to": @user_provided_url # Could be malicious
392
```
393
394
## Troubleshooting
395
396
### Common Issues
397
398
```javascript
399
// Issue: Events not firing
400
// Solution: Ensure library is loaded before DOM ready
401
402
// Issue: CSRF token errors
403
// Solution: Verify token is current and matches server expectations
404
405
// Issue: Method not working
406
// Solution: Check that all required attributes are present
407
// - data-method
408
// - data-to
409
// - data-csrf (for non-GET requests)
410
411
// Issue: Confirmation not showing
412
// Solution: Check for JavaScript errors and event propagation stopping
413
414
// Issue: Form submission not working
415
// Solution: Verify server accepts _method parameter for method override
416
```
417
418
### Debug Mode
419
420
```javascript
421
// Add debug logging
422
window.addEventListener('phoenix.link.click', function(e) {
423
console.log('Phoenix.HTML link clicked:', {
424
target: e.target,
425
method: e.target.getAttribute('data-method'),
426
to: e.target.getAttribute('data-to'),
427
confirm: e.target.getAttribute('data-confirm'),
428
csrf: e.target.getAttribute('data-csrf')
429
});
430
});
431
```
432
433
## Best Practices
434
435
1. **CSRF Tokens**: Always include valid CSRF tokens for non-GET requests
436
2. **URL Safety**: Use server-generated URLs, never user input directly
437
3. **Confirmation Messages**: Use clear, specific confirmation text
438
4. **Loading States**: Add visual feedback for method requests
439
5. **Graceful Degradation**: Ensure functionality works without JavaScript when possible
440
6. **Event Cleanup**: Remove event listeners when appropriate for SPA-style applications
441
7. **Testing**: Test with JavaScript disabled to ensure core functionality remains accessible