A nice feature of
When you need to store data in an Everlive back end, you create a "
You'll see any files already uploaded when you click on "Files". The screen shot below shows the list of files we have uploaded already (on left). The top file has been selected, and we see it's details on the right:
We can quickly see all the fields that are part of the content type by clicking the "more" icon (I'm referring to the icon at the top right of the table – with three vertical bars…which we meant to represent columns). Clicking on it allows you to add more columns to the table listing:
In addition to what you can already see
So - what does it look like for another content type to have a "File" field type? Let's look at a quick example app I threw together that allows us to upload images. This app only has one custom content type - "Images". Here's the structure:
The circled field ("Picture") is the one we're interested in. The fact that it has a type of "File" tells Everlive that any "Images" record will link (potentially) to a "Files" record. To prove this, let's look at the results of an AJAX request to retrieve one "Images" record:
$.ajax({ url: 'https://api.everlive.com/v1/wEx9wdnIcxxehNty/Images/b381a350-d2d9-11e2-b65f-2dcf34e70f16', type: "GET", success: function(data){ console.log(JSON.stringify(data, null, 2)); }, error: function(error){ console.log(JSON.stringify(error, null, 2)); } }); /* The response: { "Result": { "Title": "Yoda Dance!", "Picture": "b3511d70-d2d9-11e2-b65f-2dcf34e70f16", "CreatedAt": "2013-06-11T20:58:43.589Z", "ModifiedAt": "2013-06-11T20:58:43.589Z", "CreatedBy": "00000000-0000-0000-0000-000000000000", "ModifiedBy": "00000000-0000-0000-0000-000000000000", "Id": "b381a350-d2d9-11e2-b65f-2dcf34e70f16" } } */
Wondering what in the world those 2 extra arguments in
JSON.stringify
are? Check this out to learn more….
The response tells us that our "Yoda Dance" picture has an ID b3511d70-d2d9-11e2-b65f-2dcf34e70f16
$.ajax({ url: 'https://api.everlive.com/v1/wEx9wdnIcxxehNty/Files/b3511d70-d2d9-11e2-b65f-2dcf34e70f16', type: "GET", success: function(data){ console.log(JSON.stringify(data, null, 2)); }, error: function(error){ console.log(JSON.stringify(error)); } }); /* The response: { "Result": { "Id": "b3511d70-d2d9-11e2-b65f-2dcf34e70f16", "Filename": "Gif-Yoda-Dance.gif", "CreatedBy": "00000000-0000-0000-0000-000000000000", "Length": 511141, "ContentType": "image/gif", "CreatedAt": "2013-06-11T20:58:43.477Z", "ModifiedAt": "2013-06-11T20:58:43.477Z", "ModifiedBy": "00000000-0000-0000-0000-000000000000", "Uri": "https://api.everlive.com/v1/wEx9wdnIcxxehNty/Files/b3511d70-d2d9-11e2-b65f-2dcf34e70f16/Download" } } */
Sweet - these most definitely are the droids we're looking for. If we used the Uri
field above to include the image in our app, we'd see something similar to:
But Jim, we had to make TWO requests before we could actually include the image – one to get the Images record and another to get the Files record!
Yep - you're exactly right. And I definitely plan to explore ways we can optimize this in my next post. Let's stick to the plan for now…
Now that we've briefly looked at how Everlive stores our files (and links them to other content types), let's take a peek at the example I've put together that shows how we can upload images. You can clone your own copy here, but be aware you'll have to create your own Everlive
It's not much to look at (but it gets the job done). We have two views, a list of images (and their
Let's get to the interesting stuff right away! In the main.js file (on lines 42-92) we have a mimeMap
lookup object and an AppHelper
object which contains some helper methods we'll be using to both upload and display images:
// Lookup object we'll be using to map file // extension to mime type values var mimeMap = { jpg : "image/jpeg", jpeg: "image/jpeg", png : "image/png", gif : "image/gif" }; var AppHelper = { // produces the 'download' url for a given // file record id. This allows us, for ex, // to src an image in an img tag, etc. resolveImageUrl: function (id) { if (id) { return el.Files.getDownloadUrl(id); } else { return ''; } }, // helper function to produce the base64 // for a given file input item getBase64ImageFromInput: function (input, cb) { var reader = new FileReader(); reader.onloadend = function (e) { if (cb) cb(e.target.result); }; reader.readAsDataURL(input); }, // produces the appropriate object structure // necessary for Everlive to store our file getImageFileObject: function (input, cb) { var name = input.name; var ext = name.substr(name.lastIndexOf('.') + 1).toLowerCase(); var mimeType = mimeMap[ext]; if (mimeType) { this.getBase64ImageFromInput(input, function (base64) { var res = { "Filename": name, "ContentType": mimeType, "base64": base64.substr(base64.lastIndexOf('base64,') + 7) }; cb(null, res); }); } else { cb("File type not supported: " + ext); } } };
AppHelper.resolveImageUrl
method is simple enough. Give it an ID of a file (for example, b3511d70-d2d9-11e2-b65f-2dcf34e70f16
) and you get back the download URL for the https://api.everlive.com/v1/wEx9wdnIcxxehNty/Files/b3511d70-d2d9-11e2-b65f-2dcf34e70f16/Download
AppHelper.getBase64ImageFromInput
method uses FileReader
instance to read in the file which we want to upload and produce a base64 encoded result. This method isn't invoked directly by app code - instead it's used by the next method.AppHelper.getImageFileObject
method takes one file of a file input form element (i.e. - fileInputElement.files[0]
to get the first file) and constructs an object containing the base64 encoded image, MIME type and file name (which we'll serialize and include in the request to save it to Everlive). We find the MIME type by using our mimeMap
lookup object (we only allow jpg, All that is just infrastructural code. The actual addImageViewModel
contains the code that invokes these helpers. Let's look at that next….
In our main.js file (lines 138-190), you'll see the following code:
var addImageViewModel = { picName: '', picTitle: '', picSelected: false, onPicSet: function (e) { this.set('picSelected', true); this.set('picName', e.target.files[0].name); }, onRemovePic: function () { this.set("picSelected", false); // reset the file upload selector $newPicture = $newPicture || $("#newPicture"); $newPicture.replaceWith($newPicture = $newPicture.clone(true)); }, onAddPic: function () { $newPicture = $newPicture || $("#newPicture"); $newPicture.click(); }, saveItem: function () { var that = this; $newPicture = $newPicture || $("#newPicture"); AppHelper.getImageFileObject( $newPicture[0].files[0], function (err, fileObj) { if (err) { navigator.notification.alert(err); return; } $.ajax({ type: "POST", url: 'https://api.everlive.com/v1/wEx9wdnIcxxehNty/Files', contentType: "application/json", data: JSON.stringify(fileObj), error: function (error) { navigator.notification.alert(JSON.stringify(error)); } }).done(function (data) { var item = imagesViewModel.images.add(); item.Title = that.get('picTitle'); item.Picture = data.Result.Id; imagesViewModel.images.one('sync', function () { mobileApp.navigate('#:back'); }); imagesViewModel.images.sync(); // reset the form that.set("picSelected", false); $newPicture.replaceWith($newPicture = $newPicture.clone(true)); }); }); } };
Nevermind the fact that I'm cheating in just alerting errors. I'm reigning in the desire to make this full-fledged and, instead, focusing on show the bare necessities in uploading files.
The important method to examine saveItem
AppHelper.getImageFileObject
method, passing in the first file from our file input as the first argument. The second argument - our callback function - handles making the calls to upload the file. Assuming no errors, the second argument – fileObj
– is the object containing our file name, MIME type and base64 encoded image. We POST a request to Everlive, passing in the serialized fileObj
as the request body data. Since jQuery's AJAX call returns a promise, our done
callback is invoked when the request completes. The data
argument in our done
callback will contain the response data from uploading the file to Everlive. This is
So - inside the done
callback we're doing the following:
In our main.js file, we have an "imagesViewModel" for our list of images, and it uses the 'imageModel' for each record that appears in the list:
var imageModel = { id: 'Id', fields: { Title: { field: 'Title', defaultValue: '' }, Picture: { fields: 'Picture', defaultValue: '' } }, PictureUrl: function () { return AppHelper.resolveImageUrl(this.get('Picture')); } };
Note that the PictureUrl
method on our imageModel
calls the AppHelper.resolveImageUrl
method, passing in the ID of the Picture which we uploaded. This enables our template used to populate the list to bind the src
attribute of an img
tag to the result of this method call. You can see this in the template below, where we use the attr:{src: PictureUrl}
for the img
tag's data-bind
value.
<script type="text/x-kendo-template" id="imageTemplate"> <div> <div class="user-share"> <img data-align="left" id="Picture" data-bind="attr:{src: PictureUrl}" width="75" /> <a class="img-title">${Title}</a> </div> </div> </script>
So far we've covered:
File
field typeThis is enough to get you off the ground with storing files, but stay tuned since I plan to cover some of the options you have available in optimizing the retrieval of files, instead of increasing the number of HTTP roundtrips unecessarily.
Jim Cowart is an architect, developer, open source author, and overall web/hybrid mobile development geek. He is an active speaker and writer, with a passion for elevating developer knowledge of patterns and helpful frameworks.