or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

direct-upload.mdform-integration.mdindex.mdupload-controllers.mdutilities.md

upload-controllers.mddocs/

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

```