0
# Upload Controllers
1
2
Controller classes for managing upload workflows, event dispatching, and coordinating multiple file uploads within forms.
3
4
## Capabilities
5
6
### DirectUploadController
7
8
Controller for managing individual file uploads with event dispatching and DOM integration.
9
10
```javascript { .api }
11
/**
12
* Controller for managing individual file uploads with event dispatching
13
* Handles DOM events, hidden input creation, and upload coordination
14
*/
15
class DirectUploadController {
16
/**
17
* Creates a new DirectUploadController
18
* @param input - File input element containing the file
19
* @param file - File object to upload
20
*/
21
constructor(input: HTMLInputElement, file: File);
22
23
/**
24
* Starts the upload process and creates hidden form input
25
* @param callback - Called when upload completes or fails
26
*/
27
start(callback: (error: string | null) => void): void;
28
29
/**
30
* Handles upload progress events
31
* @param event - Progress event from XMLHttpRequest
32
*/
33
uploadRequestDidProgress(event: ProgressEvent): void;
34
35
/** Direct upload URL extracted from input's data-direct-upload-url attribute */
36
readonly url: string;
37
38
/** File input element */
39
readonly input: HTMLInputElement;
40
41
/** File being uploaded */
42
readonly file: File;
43
44
/** Underlying DirectUpload instance */
45
readonly directUpload: DirectUpload;
46
}
47
```
48
49
**Usage Examples:**
50
51
```javascript
52
import { DirectUploadController } from "@rails/activestorage";
53
54
// Manual controller usage
55
const fileInput = document.querySelector("input[type=file]");
56
const file = fileInput.files[0];
57
58
const controller = new DirectUploadController(fileInput, file);
59
60
controller.start((error) => {
61
if (error) {
62
console.error("Upload failed:", error);
63
} else {
64
console.log("Upload completed successfully");
65
// Hidden input with signed_id has been created
66
}
67
});
68
69
// Listen for controller events
70
fileInput.addEventListener("direct-upload:progress", (event) => {
71
const { progress, file } = event.detail;
72
console.log(`${file.name}: ${Math.round(progress)}%`);
73
});
74
75
fileInput.addEventListener("direct-upload:error", (event) => {
76
const { error, file } = event.detail;
77
console.error(`Failed to upload ${file.name}:`, error);
78
});
79
```
80
81
### DirectUploadsController
82
83
Controller for managing multiple file uploads within a single form, coordinating sequential uploads and form submission.
84
85
```javascript { .api }
86
/**
87
* Controller for managing multiple file uploads in a form
88
* Coordinates sequential uploads and handles form submission
89
*/
90
class DirectUploadsController {
91
/**
92
* Creates a new DirectUploadsController
93
* Automatically finds all file inputs with data-direct-upload-url in the form
94
* @param form - Form element containing file inputs
95
*/
96
constructor(form: HTMLFormElement);
97
98
/**
99
* Starts uploading all files sequentially
100
* @param callback - Called when all uploads complete or first error occurs
101
*/
102
start(callback: (error?: string) => void): void;
103
104
/**
105
* Creates DirectUploadController instances for all files
106
* @returns Array of DirectUploadController instances
107
*/
108
createDirectUploadControllers(): DirectUploadController[];
109
110
/** Form element being managed */
111
readonly form: HTMLFormElement;
112
113
/** Array of file input elements with files selected */
114
readonly inputs: HTMLInputElement[];
115
}
116
```
117
118
**Usage Examples:**
119
120
```javascript
121
import { DirectUploadsController } from "@rails/activestorage";
122
123
// Manual form upload management
124
const form = document.querySelector("form");
125
const controller = new DirectUploadsController(form);
126
127
// Start uploads for all files in form
128
controller.start((error) => {
129
if (error) {
130
console.error("Upload failed:", error);
131
// Re-enable form inputs
132
enableFormInputs(form);
133
} else {
134
console.log("All uploads completed");
135
// Form can now be submitted normally
136
form.submit();
137
}
138
});
139
140
// Listen for form-level events
141
form.addEventListener("direct-uploads:start", () => {
142
console.log("Starting uploads...");
143
showLoadingSpinner();
144
});
145
146
form.addEventListener("direct-uploads:end", () => {
147
console.log("All uploads completed");
148
hideLoadingSpinner();
149
});
150
```
151
152
### Event Dispatching
153
154
Both controller classes dispatch custom DOM events to provide upload progress and status information.
155
156
**DirectUploadController Events** (dispatched on input element):
157
158
```javascript { .api }
159
interface DirectUploadControllerEvents {
160
/** Dispatched when controller is initialized */
161
"direct-upload:initialize": {
162
detail: { id: number; file: File };
163
};
164
165
/** Dispatched when upload starts */
166
"direct-upload:start": {
167
detail: { id: number; file: File };
168
};
169
170
/** Dispatched before blob creation request */
171
"direct-upload:before-blob-request": {
172
detail: { id: number; file: File; xhr: XMLHttpRequest };
173
};
174
175
/** Dispatched before file storage request */
176
"direct-upload:before-storage-request": {
177
detail: { id: number; file: File; xhr: XMLHttpRequest };
178
};
179
180
/** Dispatched during upload progress */
181
"direct-upload:progress": {
182
detail: { id: number; file: File; progress: number };
183
};
184
185
/** Dispatched when upload error occurs */
186
"direct-upload:error": {
187
detail: { id: number; file: File; error: string };
188
};
189
190
/** Dispatched when upload completes */
191
"direct-upload:end": {
192
detail: { id: number; file: File };
193
};
194
}
195
```
196
197
**DirectUploadsController Events** (dispatched on form element):
198
199
```javascript { .api }
200
interface DirectUploadsControllerEvents {
201
/** Dispatched when upload process begins */
202
"direct-uploads:start": {
203
detail: {};
204
};
205
206
/** Dispatched when all uploads complete */
207
"direct-uploads:end": {
208
detail: {};
209
};
210
}
211
```
212
213
**Event Handling Examples:**
214
215
```javascript
216
// Track individual file progress
217
document.addEventListener("direct-upload:progress", (event) => {
218
const { id, file, progress } = event.detail;
219
updateFileProgress(id, file.name, progress);
220
});
221
222
// Handle upload errors with custom UI
223
document.addEventListener("direct-upload:error", (event) => {
224
const { file, error } = event.detail;
225
226
// Prevent default alert
227
event.preventDefault();
228
229
// Show custom error message
230
showErrorToast(`Failed to upload ${file.name}: ${error}`);
231
});
232
233
// Modify requests before they're sent
234
document.addEventListener("direct-upload:before-blob-request", (event) => {
235
const { xhr } = event.detail;
236
xhr.setRequestHeader("X-Custom-Header", "value");
237
});
238
239
// Track upload bandwidth
240
document.addEventListener("direct-upload:before-storage-request", (event) => {
241
const { xhr, file } = event.detail;
242
const startTime = Date.now();
243
244
xhr.upload.addEventListener("progress", (progressEvent) => {
245
const elapsed = Date.now() - startTime;
246
const loaded = progressEvent.loaded;
247
const speed = loaded / elapsed * 1000; // bytes per second
248
console.log(`${file.name} upload speed: ${formatBytes(speed)}/s`);
249
});
250
});
251
```
252
253
### Hidden Input Management
254
255
DirectUploadController automatically creates hidden form inputs containing the signed blob IDs for successful uploads.
256
257
**Hidden Input Creation:**
258
259
```javascript
260
// When upload completes successfully, controller creates:
261
// <input type="hidden" name="original_input_name" value="signed_blob_id">
262
263
// For example, if original input was:
264
// <input type="file" name="post[attachments][]" data-direct-upload-url="...">
265
266
// Controller creates:
267
// <input type="hidden" name="post[attachments][]" value="eyJfcmFpbHMiOnsibWVzc2F...">
268
```
269
270
**Manual Hidden Input Handling:**
271
272
```javascript
273
import { DirectUploadController } from "@rails/activestorage";
274
275
class CustomDirectUploadController extends DirectUploadController {
276
start(callback) {
277
// Override to customize hidden input creation
278
super.start((error) => {
279
if (!error) {
280
// Custom logic after successful upload
281
this.createCustomHiddenInput();
282
}
283
callback(error);
284
});
285
}
286
287
createCustomHiddenInput() {
288
const hiddenInput = document.createElement("input");
289
hiddenInput.type = "hidden";
290
hiddenInput.name = "custom_attachment_ids[]";
291
hiddenInput.value = this.directUpload.blob.signed_id;
292
hiddenInput.dataset.filename = this.file.name;
293
hiddenInput.dataset.contentType = this.file.type;
294
295
this.input.parentNode.insertBefore(hiddenInput, this.input);
296
}
297
}
298
```
299
300
### Error Handling and Recovery
301
302
Controllers provide comprehensive error handling with customizable recovery options.
303
304
**Error Types:**
305
306
```javascript
307
// Network errors
308
"Error creating Blob for \"file.jpg\". Status: 422"
309
"Error storing \"file.jpg\". Status: 403"
310
311
// File system errors
312
"Error reading file.jpg"
313
314
// Server errors
315
"Error creating Blob for \"file.jpg\". Status: 500"
316
```
317
318
**Custom Error Handling:**
319
320
```javascript
321
class RobustDirectUploadsController extends DirectUploadsController {
322
start(callback) {
323
let retryCount = 0;
324
const maxRetries = 3;
325
326
const attemptUpload = () => {
327
super.start((error) => {
328
if (error && retryCount < maxRetries) {
329
retryCount++;
330
console.log(`Upload failed, retrying... (${retryCount}/${maxRetries})`);
331
setTimeout(attemptUpload, 1000 * retryCount); // Exponential backoff
332
} else {
333
callback(error);
334
}
335
});
336
};
337
338
attemptUpload();
339
}
340
}
341
342
// Usage
343
const robustController = new RobustDirectUploadsController(form);
344
robustController.start((error) => {
345
if (error) {
346
console.error("Upload failed after retries:", error);
347
} else {
348
console.log("Upload successful");
349
}
350
});
351
```
352
353
### Sequential Upload Management
354
355
DirectUploadsController uploads files sequentially rather than in parallel to avoid overwhelming the server and provide predictable progress tracking.
356
357
**Upload Sequence:**
358
359
```javascript
360
// Files are uploaded one at a time in this order:
361
// 1. File A: checksum → blob creation → storage upload
362
// 2. File B: checksum → blob creation → storage upload
363
// 3. File C: checksum → blob creation → storage upload
364
// 4. All complete → callback with no error
365
366
// If any file fails, remaining files are not uploaded
367
// and callback is called immediately with error
368
```
369
370
**Custom Parallel Upload Controller:**
371
372
```javascript
373
class ParallelDirectUploadsController extends DirectUploadsController {
374
start(callback) {
375
const controllers = this.createDirectUploadControllers();
376
const results = [];
377
let completedCount = 0;
378
let hasError = false;
379
380
this.dispatch("start");
381
382
controllers.forEach((controller, index) => {
383
controller.start((error) => {
384
if (hasError) return; // Skip if already failed
385
386
if (error) {
387
hasError = true;
388
callback(error);
389
this.dispatch("end");
390
return;
391
}
392
393
results[index] = true;
394
completedCount++;
395
396
if (completedCount === controllers.length) {
397
callback();
398
this.dispatch("end");
399
}
400
});
401
});
402
403
// Handle case where no controllers exist
404
if (controllers.length === 0) {
405
callback();
406
this.dispatch("end");
407
}
408
}
409
}
410
```