File Uploads in Ember Data
Recently I’ve been working on an EmberJS application (SalesFlip - go register there now) which needs to allow file uploads. There are a couple of different approaches to this which I’ll discuss here, along with a new one which I would like to propose. If you are wondering what all the import and export statements are in the examples below, it’s because I’m using Ember App Kit. You should use it too!
HTML5 FileReader API
https://developer.mozilla.org/en-US/docs/Web/API/FileReader
The HTML5 FileReader API is fine for really small file uploads outside of models. It seems to be the currently recommended solution on StackOverflow. Just create an UploadFile view and then bind the file attribute to the relevant attribute in the model. However, the ember-data adapter will then upload the file as a data URI via JSON. This proved pretty much useless for me. Anything over around 60k would fail.
app/models/user.coffee
User = DS.Model.extend
avatar: DS.attr('string')
`export default User`
app/routes/users/new.coffee
UsersNewRoute = Ember.Route.extend
model: ->
@store.createRecord('user')
actions:
save: ->
@currentModel.save()
`export default UsersNewRoute`
app/templates/users/new.hbs
view "upload-file" name="avatar" fileBinding="avatar"
app/views/upload_file.coffee
UploadFile = Ember.TextField.extend
tagName: 'input'
attributeBindings: ['name']
type: 'file'
file: null
change: (e) =>
reader = new FileReader()
reader.onload = (e) =>
fileToUpload = e.target.result
Ember.run =>
@set 'file', fileToUpload
reader.readAsDataURL(e.target.files[0])
`export default UploadFile`
Custom FormData Upload View
With this approach (adapted from here) we define an upload button view which will upload the file on change. This would require some backend magic to work for new users (ones without an ID). Perhaps the file could be saved and then associated with the user when the user is saved.
app/templates/users/new.hbs
view "upload-button" userBinding="user"
app/views/upload_button.coffee
UploadButton = Ember.View.extend
tagName: 'input'
attributeBindings: ['type']
type: 'file'
originalText: 'Upload Finished Product'
uploadingText: 'Busy Uploading...'
newItemHandler: (data) ->
@get('controller.store').push('item', data)
preUpload: ->
parent = @.$().closest('.fileupload-addbutton')
upload = @get('uploadingText')
parent.addClass('disabled')
@.$().css('cursor', 'default')
@.$().attr('disabled', 'disabled')
postUpload: ->
parent = @.$().closest('.fileupload-addbutton')
form = parent.closest('#fake_form_for_reset')[0]
orig = @get('originalText')
parent.removeClass('disabled')
@.$().css('cursor', 'pointer')
@.$().removeAttr('disabled')
form.reset()
change: (e) ->
formData = new FormData()
@preUpload()
formData.append('user_id', @get('user.id'))
formData.append('file', @.$().get(0).files[0])
$.ajax
url: '/file_upload_handler/'
type: 'POST'
# Ajax events
success: (data) =>
@postUpload()
@newItemHandler(data)
error: =>
@postUpload()
alert('Failure')
# Form data
data: formData
# Options to tell jQuery not to process data or worry about content-type.
cache: false
contentType: false
processData: false
`export default UploadButton`
Ember Data FormData Adapter
This is the approach that I have settled on. I created a FormData Adapter which is used by any models which require file upload. Using this approach, adding file uploads to another file is just a matter of creating a new adapter for it which extends the FileUpload adapter. Although I haven’t tried it, it should be easy to combine this approach with the HTML5 File Reader approach to generate image previews and validate file information before uploading.
First define a transform so that files are left untouched
app/transforms/file.coffee
FileTransform = DS.Transform.extend
serialize: (jsonData) ->
jsonData
deserialize: (externalData) ->
externalData
`export default FileTransform`
Use the newly defined “file” attribute type
app/models/user.coffee
User = DS.Model.extend
avatar: DS.attr('file')
`export default User`
Create a FileUpload input. This is what assigns the file to the model
app/views/file_upload.coffee
FileUploadView = Ember.View.extend
content: null
type: 'file'
change: (event) ->
if event.target.files.length > 0
@set('content', event.target.files[0])
else
@set('content', null)
`export default FileUploadView`
app/templates/users/new.hbs
view "file-upload" contentBinding="avatar"
Create a FormDataAdapter. This is what handles uploading the data via FormData instead of the default JSON.
app/adapters/form_data.coffee
`import ApplicationAdapter from 'appkit/adapters/application'`
get = Ember.get
FormDataAdapter = ApplicationAdapter.extend
ajaxOptions: (url, type, hash) ->
hash = hash || {}
hash.url = url
hash.type = type
hash.dataType = 'json'
hash.context = @
if hash.data and type != 'GET' and type != 'DELETE'
hash.processData = false
hash.contentType = false
fd = new FormData()
root = Object.keys(hash.data)[0]
for key in Object.keys(hash.data[root])
if hash.data[root][key]
fd.append("#{root}[#{key}]", hash.data[root][key])
hash.data = fd
headers = get(@, 'headers')
if headers != undefined
hash.beforeSend = (xhr) ->
for key in Ember.keys(headers)
xhr.setRequestHeader(key, headers[key])
hash
`export default FormDataAdapter`
Create a UserAdapter for the User model to use
app/adapters/user.coffee
`import FormDataAdapter from 'appkit/adapters/form_data'`
UserAdapter = FormDataAdapter.extend()
`export default UserAdapter`
Need some extra development power?
I don't have a lot of time at the moment, but I'm always interested to hear about new projects. I'm particularly interested in EmberJS/Elixir projects and refactoring Rails monoliths. Drop me a line at hello [at] mattbeedle [dot] name or using my contact form http://www.mattbeedle.name/#page-contact.
Tweetcomments powered by Disqus