Introduction
In this article, we want to share with you one of the components of Codeline, namely A Drag and Drop File Attachment Module, so you can use it for your projects as well. It is very important to us to provide our customers a convenient way of uploading files so that they can easily and quickly attach the files related to their projects. This can be images, mockups, design files, archives and even video files with instructions. On Codeline, we use this module in our Task Management System to upload and attach different types of files very quickly. So let’s begin with the code and its integration.
Stack
We use Laravel Framework and handle REST API requests and building user interfaces with Aurelia. The instructions below contain the implementation of both Laravel and Aurelia parts.
Create Aurelia Components
The steps below are described according to our project structure and may be different from your project, so it should be integrated accordingly. We keep all the components inside the components folder.
We created two components. The first one is File Upload Box Component which is for uploading files. The second one is Attachments Component to display uploaded files.
File Upload Box Component
This component contains two files, typescript class, and template view files.
1. First let’s create a directory inside components folder file-upload-box, save a new file file-upload-box.ts inside it and create a new class (full code included in source files below).
import {autoinject, bindable, customElement, computedFrom, bindingMode} from "aurelia-framework"; import {Confirmation} from "../../core/wrappers/confirmation"; import {HttpService} from "../../core/services/http-service"; import {PopupMessages} from "../../core/wrappers/popup-messages"; import {EventAggregator, Subscription} from "aurelia-event-aggregator"; import {App} from "../../app"; import * as $ from "jquery"; @customElement('file-upload-box') @bindable('deleteUrl') @bindable('attachment') @bindable('removeFileEvent') @autoinject() export class FileUploadBox { attachment: any; uploading: boolean = false; progress: number = 0; dropSubscription: Subscription; selectedFile: any; targetId: any; ... constructor(private app: App, private confirmation: Confirmation, private httpService: HttpService, private popup: PopupMessages, private event: EventAggregator, private element: Element) { this.targetId = $(this.element).attr('au-target-id'); this.dropChannel = 'drop-files:portfolio-manager-' + this.targetId; } attached() { this.dropSubscription = this.event.subscribe("drop-files:portfolio-manager-" + this.targetId, (files) => { this.selectedFile = files[0]; this.uploadFiles(); }); } ... private uploadFiles() { this.uploading = true; return this.httpService.uploadFile(this.selectedFile, (p) => { this.progress = p; }).then((tempFile) => { this.progress = 100; this.uploading = false; this.tempFile = tempFile; return; }); } async removeFile() { ... if (this.attachment) { ... return this.httpService.client.delete(this.deleteUrl).then((response) => { this.popup.success("Attachment is deleted"); this.event.publish(this.removeFileEvent); console.log("published"); }).catch((error) => { console.log(error); this.popup.error("Unexpected Error"); }); } } ... }
2. Create a template view file file-upload-box.html
<template> <require from="components/attachments/attachment-droppable"></require> <require from="components/progress-bar/progress-bar"></require> <require from="core/converters/mime-icon"></require> <div class="panel"> <figure if.bind="showDroppable" style="min-height: 300px" attachment-droppable="visible.bind: 'true'; drop-event-channel.bind: dropChannel;"> </figure> <ul if.bind="!showDroppable" class="list-group list-group-dividered"> <li class="list-group-item"> <a click.delegate="removeFile()" class="badge" show.bind="!uploading">X</a> <span if.bind="!attachment"><i class="icon wb-file"></i> ${selectedFile.name} </span> <span if.bind="attachment"><i class="icon wb-file"></i> <a target="_blank" href="${attachment.signed_url}">${attachment.file_name}</a></span> </li> </ul> <div class="time pull-right" if.bind="!showDroppable && attachment">${attachment.created_at}</div> <div class="text-truncate" if.bind="!showDroppable && attachment"> </div> </div> <progress-bar if.bind="uploading && showDroppable" value.bind="progress"></progress-bar> </template>
Attachments Component
Create a directory inside components folder attachments. This component contains three files: attachment-droppable.ts, attachments.ts, attachments.html.Droppable Area (attachment-droppable.ts). This component file is used to create a container where the files can be dropped, so the system starts automatically uploading them.
1. Droppable Area (attachment-droppable.ts). This component file is used to create a container where the files can be dropped, so the system starts automatically uploading them (full code included in source files below).
import {customAttribute, bindable, inject, DOM} from 'aurelia-framework'; import {EventAggregator} from "aurelia-event-aggregator"; ... @customAttribute('attachment-droppable') @inject(DOM.Element, EventAggregator) export class AttachmentDroppable { @bindable dropEventChannel; ... dragTimer: any; dropTargetHtml: any; constructor(private element: Element, private ea: EventAggregator) { this.$el = $(element); } ... attached() { if (this.attach == false) { return false; } ... this.attachArea(); let self = this; if (this.visible) { this.$container.on('click', function (e: any) { e.preventDefault(); e.stopPropagation(); let fileTrigger = self.$el.parent().find('#fileSelector').first(); fileTrigger.trigger('click'); }); this.$container.parent().find('#fileSelector').on('change', function (e: any) { self.ea.publish(self.dropEventChannel, e.target.files); self.$container.off('click'); $(e.target).remove(); }); } ... } isFileDrag(e) { var dt = e.originalEvent.dataTransfer; if (dt.types) { return dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('Files'); } return false; } ... }
Visually it looks like this:

2. Attachments Class (attachments.ts). This component file is used to define all required methods to display the list of attached files and pass them to the template file (full code included in source files below).
import {bindable, customElement, inject} from "aurelia-framework"; import * as _ from "lodash"; import {ReadableSizeValueConverter} from "../../core/converters/readable-size"; import {HttpService} from "../../core/services/http-service"; import {PopupMessages} from "../../core/wrappers/popup-messages"; @customElement("attachments") @bindable({name: 'maxSizeWarning', defaultValue: true}) @bindable({name: 'prompt', defaultValue: 'File Attachments'}) @bindable({name: "maxFiles", defaultValue: 1}) @bindable({name: "maxSize", changeHandler: "setMaxSize"}) // in bytes @bindable({name: "files", defaultValue: []}) @bindable({name: "allowedFileTypes", defaultValue: "*/*"}) @inject(PopupMessages, HttpService, ReadableSizeValueConverter) export class Attachments { uploading: boolean = false; progress: number = 0; private addedFiles: FileList; maxFiles: number = 0; maxSize: number = 0; files: File[] = []; prompt: string = "File Attachments"; upload() { this.progress = 0; if (this.files.length == 0) { this.uploading = false; return Promise.resolve([]); } this.progress = 0; this.uploading = true; return this.httpService.uploadFiles(this.files, (progress) => { this.progress = progress; }).then((tempFiles) => { this.progress = 100; this.uploading = false; return tempFiles; }); } ... }
3. Attachments Template File (attachments.html). This component file is used to display the rendered HTML part.
<template> <require from="core/converters/mime-icon"></require> <require from="core/converters/readable-size"></require> <require from="components/progress-bar/progress-bar"></require> <div class="form-group"> <label>${ prompt } <span show.bind="maxFiles > 1">(${files.length}/${ maxFiles })</span></label> <p show.bind="maxSizeWarning && maxSize > 0"><small>${ maxFiles > 1 ? 'Each file' : 'The file' } should be no larger than ${ maxSize | readableSize }</small></p> <progress-bar show.bind="uploading" value.bind="progress" label="Uploading..."></progress-bar> <div class="input-group input-group-file" show.bind="!uploading"> <span class="input-group-btn"> <span class="btn btn-info btn-file"> <i class="icon wb-upload" aria-hidden="true"></i> Attach File(s) <input id="attachment" type="file" name="" multiple="" files.bind="addedFiles" change.delegate="add()" accept="${allowedFileTypes}"> </span> </span> </div> <ul class="list-group list-group-dividered"> <li class="list-group-item" repeat.for="file of files"> <a click.delegate="remove($index)" class="badge" show.bind="!uploading">X</a> <i class="icon ${ file.type | fileTypeIcon }"></i> ${ file.name } </li> </ul> </div> </template>
Here is a screenshot of the list of attachments:

Dependent Components
The File Upload Box and Attachments Components have some dependent components which will be described below.
1. HTTP Client (http-service.ts)
We use this component to send the selected files to the server, which will be handled by the Laravel Framework. The system uses Jquery Library to call its Ajax Request methods to send selected files to the server and get a response with the information about uploaded files.
2. Popup Messages (popup-messages.ts)
The Popup Component uses Toastr library to notify users of a file uploaded status.
3. Aurelia Event Aggregator
We use this component to attach custom events on each stage of the uploading process to pass the data to other components and re-render them accordingly.
4. Confirmation Popup (confirmation.ts)
The Confirmation Popup uses Aurelia Dialog Component to display a modal form when a user tries to delete attachments.
5. Progress Bar (progress-bar.ts)
We use the Progress Bar Component to visualize the progression of an uploading process of each file.
Please note that all dependent files are included in the source files archive below.
Handle Server-Side File Upload using Laravel Framework
The Laravel Framework handles the files sent by Aurelia by providing its API. The system stores all uploaded files on AWS S3 Storage. Let’s check how it was implemented with the example of our Codeline Task Management System.
1. Let’s create a new controller to handle new attachments (AttachmentsController.php) with two methods: store and destroy. Since we use attachments in our Task Management System, we pass the task details additionally.
<?php namespace App\Http\Controllers; use App\Events\StoriesUpdated; use App\Models\Attachment; use App\Models\Story; use App\Models\Task; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; class AttachmentsController extends Controller { public function store(Task $task, Request $request) { $this->authorize('attach-files', $task); $attachment = $task->attachFile($request->all()); return response()->json($attachment); } public function destroy(Task $task, Attachment $attachment) { $task->load('project'); $this->authorize('delete-attachments', $task); DB::transaction(function () use ($task, $attachment) { $attachment->delete(); Story::where('reference_id', $attachment->id) ->whereIn('type', ['attachment', 'attachment_draft']) ->get() ->each(function ($story) { $story->delete(); }); event(new StoriesUpdated($task)); }); return response()->json($attachment); } }
2. Define new resource in routes.php
// Attachment Resources Route::resource('tasks.attachments', 'AttachmentsController', ['only' => ['store', 'destroy']]);
3. Store uploaded files (Task Model Class with an attachFile method)
class Task extends Model { public function attachFile($file, $public = true) { $attachment = Attachment::attachTemporaryUpload($file, $this); Story::create([ 'task_id' => $this->id, 'type' => 'attachment', 'action' => 'updated', 'text' => $attachment->file_name, 'html_text' => $attachment->file_name, 'reference_id' => $attachment->id, 'payload' => $attachment, ]); event(new StoriesUpdated($this)); return $attachment; }
4. Attachment Model with attachTemporaryUpload method used to upload files to S3 AWS Storage.
<?php namespace App\Models; use Exception; use ReflectionClass; use Illuminate\Support\Facades\Cache; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; use App\Services\Search\Concerns\Searchable; class Attachment extends Model { public static function attachTemporaryUpload($temporaryUpload, $model, $public = true) { $fileMeta = Cache::get($temporaryUpload['uploadId']); if (!$fileMeta) { throw new Exception('Temporary file upload has expired or not found'); } $attachment = new Attachment(); $attachment->file_name = $fileMeta['name']; $attachment->file_size = $fileMeta['size']; $attachment->file_type = $fileMeta['type']; $attachment->public = $public; $model->attachments()->save($attachment); $contents = file_get_contents(storage_path('app/' . config('filesystems.temporary_path') . $temporaryUpload['uploadId'])); $destPath = self::fileLocation($attachment, $model); Storage::disk('s3')->put($destPath, $contents); Cache::forget($temporaryUpload['uploadId']); return $attachment; } }
Conclusion
As you can see, in the examples above how easy to extend Aurelia Framework, implement powerful File Uploader and connect it with Laravel Framework. This component was implemented exclusively for our purposes and can be extended based on your needs. Feel free to ask any questions you might have in the comment box below, so we can assist you.