accept: will be bound to the input’s accept attribute, which is used
to limit the type of files the browser’s dialog will show to the user.

multiple: will be bound to the input’s multiple attribute, which
tells the browser’s dialog if it should support selection of multiple files
or not.

files: will be bound to the input’s files attribute. This property is
bound two way by default, so the file(s) selected by the user are assigned
back to the bound property.

The view-model also declares an input property, to which the template will
assign a reference on the <input type="file"> element.

Lastly, since the input’s files property is read-only and the DOM API
doesn’t expose a method to clear the input’s file selection (other than
calling the reset method on the whole surrounding form), the view-model
uses a hack to clear the selected files when an empty value is assigned
to the file-picker’s files property: it sets the input’s type to
an empty string then resets it back to file.

The file-picker’s template defines an <input type="file"> element,
styled so it is invisible and so it occupies no space in the DOM.
Its accept, multiple, and files attributes are also properly
bound to their corresponding property on the view-model. Lastly, it
assigns a reference on the input to the view-model’s input
property.

The template also declares a button element, styled using Bootstrap’s
classes. Inside it, a default content projection slot, with the
Select text as its default content. Additionally, the button’s
click event calls the input’s click method. Thanks to this,
the browser’s file dialog will show up when the user clicks the
button, even though the input element is not visible.

This component basically just replaces the ugly native file picker
with a sexier button.

Adding a file drop target

Next, let’s create a custom attribute allowing to transform any
element into a file drag and drop target:

The attribute’s target element will be injected in the view-model’s
constructor. When the attribute is attached to the DOM, it starts
listening for the dragover, drop, and dragend events on the
target element. When the attribute is detached from the DOM, the
event listeners are removed.

The attribute is bound two way by default, so the file(s) assigned
to its value when a user drops them on the target element are
assigned back to the bounded property, if any. However, upon files
being dropped on the target element, the view-model checks if the
value is a function or not. This means that the attribute can be
used either with the .bind command, so the dropped files are
assigned to the bound expression, or with the .call command, so
the bound expression is called and passed the dropped files whenever
a drop event occurs.

Chunking an array

In order to display the selected images as a gallery, we’ll use
Bootstrap’s grid system. This means we’ll need to break the files
array down in chunks, so we can iterate on chunks to render rows,
then on each chunk’s files to render columns.

The chunk value converter expects an array and the chunks’ size
as its parameter and returns an array of array.

Displaying a Blob object as an image

The last part we’ll need is some way to display a File instance
inside an img element. To do this, we’ll leverage the browser’s
URL.createObjectURL function, which takes a Blob object as a
parameter and returns a special URL leading to this resource. Our
custom attribute, which will be used essentially on img elements,
will be bound to a Blob object, will generate an object URL from it,
and will assign this URL to the img element’s src attribute.

Some of you might think that a value converter would be a better fit for
this type of feature, and I would absolutely agree. A value converter
could take as an input a Blob object and return the object URL. It
could then be used on a binding between an img element’s src
attribute and a property containing a Blob object.

However, in this particular case, each object URL must be released after
usage in order to prevent memory leaks, and value converters offer no
mechanism to be notified when a value is no longer used. On the contrary,
HTML behaviors offer a much richer workflow and a wider set of extension
points. That’s why we will create a custom attribute instead:

The template starts with a jumbotron container, which acts as a
file-drop-target. When files are dragged and dropped on this element,
the view-model’s add method will be called and passed the dropped
files.

Inside this container, the files array is rendered on three columns
using the chunk value converter, each file displayed inside a Bootstrap
card component. Each card displays the file in an img element
using the blob-src attribute, a button whose click event calls the
view-model’s remove method, and the file’s name.

Lastly, underneath the image gallery, a file-picker element allows
the user to select image files. The selected files are bound to the
view-model’s selectedFiles property, then the change event
dispatched by the underlying <input type="file"> element and bubbling
up the DOM triggers a call to the addSelectedFiles method. The
file-picker’s default projection slot is also overwritten with the text
Add.

The view-model declares a files bindable property, which is bound
two way by default. This property is expected to initially contain
an empty array.

When files are dropped on the drop target element, the add method
is called and the dropped files are appended to the files property.
When the user selects files using the file-picker, the selected files
are assigned back to the selectedFiles property, then the change
event handler calls the addSelectedFiles, which appends the
selectedFiles to the files property, and finally assigns null to
the selectedFiles.

This last step makes sure that the underlying <input type="file">
element has its selection cleared. Without it, if a user tries to add
the same file twice in a row, the change event would not be triggered
the second time, because the input’s value would not change, so the
second file selection would fail from the user’s perspective.

Using the image files picker

Using the image-files-picker element is then pretty simple. We first
need to declare a property hosting the array of files on the App
view-model:

app.ts:

exportclassApp{files:File[]=[];}

Next, we simply need to add the custom element in the template of
our App component:

Of course, the various parts need to be loaded, either using
the require statement in the app.html template, or in the
resources/index.ts feature’s configure function.

Filtering out non-image files

At this point, a user can select or drop any type of files using our
component. Some logic allowing only image files should be somehow added.

A basic filtering logic, using the same syntax as the <input type="file">
element’s accept attribute, is implemented in the complete code sample,
which you can find
here.
A more complete solution, showing error messages to the user, can easily be
implemented. I’ll leave this as an exercise to the reader.

Exploiting the selected images

Typically, such a component would be used to first select a bunch of image files,
then to upload those files to some remote endpoint. This is pretty easy to do with
Aurelia’s Fetch client and the FormData class from the Fetch API.

Here’s an example of a client service used to upload an array of File instances
to some remote endpoint:

The Mozilla Developer Network has
some great doc
about the FormData class.

Summary

Once again, Aurelia makes things easy. Its various constructs, such as custom attributes,
elements, and value converters, help us decompose a problem and solve each of its parts
with a generic, reusable solution, and then recombine them together to address our initial,
specific problem. Shameless plug alert: this aspect is one of the many topics addressed
in Learning Aurelia. You should
definitely give it a look!

Update 2018-03-02

Reader @sokratismanolis pointed out in this comment that the code
doesn’t work on IE 11 and Edge. I didn’t have time to find out the core of the issue yet, but
it seems to be caused by a delegated event listener being fired before a data binding instruction
is refreshed. I worked around the issue by removing the delegated event listener and by using
the @observable attribute on selectedFiles, as you can see on
this branch.