
Save Prestashop Custom fields on “Add to cart” – Part 2
After tackling text fields, in this second part we will see how to add ajax uploads for file customizations in Prestashop.
- Version used: Prestashop 1.6
« Save Prestashop Custom fields on “Add to cart” – Part 1
Continuing from what we did in the previous tutorial, instead of modifying the ajax-cart.js file, this time we will handle product.js, located in the theme folder /js/. To make things easier we will also add atrigger button to fire the upload event, and modify our previously amended tools.js.
Before starting, it’s important to notice this kind of upload will not work for older browsers (like ie9), so you might want to add some kind of warning, or completely hide the ajax button in this case.
Handling ajax file uploads in Prestashop – product.js
First off, let’s create our small trigger. Locate product.tpl in your theme’s folder, open it up and right before this
</div> {/if} {if $product->text_fields|intval}
Add the following
<a class="btn btn-default" href="javascript:void(0)" id="uploadTrigger">Upload</a>
If your theme looks different, make sure it’s inside this conditional, at least:
{if $product->uploadable_files|intval} ... {/if}
Then, let’s deal with Javascript.
Open up product.js and make sure you put your cursor inside the very first $(document).ready(function() block. The first thing we need to do is create a list of files that the user might want to upload, as soon as the file inputs change:
var files = new Array(); $('.customizationUploadLine').find('input[type="file"]').on('change', prepareUpload); function prepareUpload(event) { files.push({'name' : event.target.name, 'file' :event.target.files[0]}); }
Explanation: we are taking advantage of the ‘files’ property of the change event, which holds the file(s) currently being added to the clicked input. We want to record modifications to all our file customization fields, and push the files array with each of the possible uploads. We will need each entry to be named as the form field, so we use a new object for each file, holding the correct name in the ‘name’ property, and the actual file content in the ‘file’ one.
The original method I used came from this tutorial, in case you want to have a reference of the source code: Ajax file uploads with jQuery
Now, let’s target the upload button’s click;
$('#uploadTrigger').click(function(e) { if(files.length > 0) { // here will be out ajax } // end checking files length else alert('Nothing to upload!'); });
We have to make sure we have at least one file to upload; otherwise it’s useless to continue with the call.
Since we do not know how long the file upload will take, it’s better to prevent the user from taking further actions before it’s completed. Let’s add some feedback:
$('<div class="myoverlay"></div>').css({ 'position' : 'fixed', 'top' : 0, 'left' : 0, 'background' : 'black', 'background' : 'rgba(0,0,0,.5)', 'z-index' : 5999, 'width' : '100%', 'height' : '100%', 'cursor' : 'pointer' }).appendTo('body'); $('<div class="uploadingfiles">Your files are being uploaded...<img src="'+baseUri+'themes/default-bootstrap/img/ajax-loader.gif"></div>') .css({ 'position' : 'absolute', 'top' : '30%', 'left' : '50%', 'width' : '300px', 'margin-left' : '-150px', 'text-align' : 'center', 'padding' : '10px', 'background' : 'white' }) .appendTo('.myoverlay');
Now the tricky part. We will use the new HTML5, supported with XHR2 (that is why it will not work on old browsers!). Right after appending the box, add the following:
var data = new FormData(); $.each(files, function(key, obj) { data.append(obj.name, obj.file); }); data.append('submitCustomizedDatas', 1); data.append('ajax', 1);
Explanation: as mentioned, we need a FormData object to hold our data. Then, we iterate through each of the files, assigning them to the respective name. The append method works like data.append(key, data). Lastly, since in the previous tutorial we only ran the customization scripts when having two specific queries sent to the server, we also append them to our form data. The value here is not important, as long as it’s more than 0.
The ajax call
Here is our code so far:
var files = new Array(); $('.customizationUploadLine').find('input[type="file"]').on('change', prepareUpload); // Grab the files and set them to our variable function prepareUpload(event) { files.push({'name' : event.target.name, 'file' :event.target.files[0]}); } $('#uploadTrigger').click(function(e) { if(files.length > 0) { $('<div class="myoverlay"></div>').css({ 'position' : 'fixed', 'top' : 0, 'left' : 0, 'background' : 'black', 'background' : 'rgba(0,0,0,.5)', 'z-index' : 5999, 'width' : '100%', 'height' : '100%', 'cursor' : 'pointer' }).appendTo('body'); $('<div class="uploadingfiles">Your files are being uploaded...<img src="'+baseUri+'themes/default-bootstrap/img/ajax-loader.gif"></div>') .css({ 'position' : 'absolute', 'top' : '30%', 'left' : '50%', 'width' : '300px', 'margin-left' : '-150px', 'text-align' : 'center', 'padding' : '10px', 'background' : 'white' }) .appendTo('.myoverlay'); var data = new FormData(); $.each(files, function(key, obj) { data.append(obj.name, obj.file); }); data.append('submitCustomizedDatas', 1); data.append('ajax', 1); } // end checking files length else alert('Nothing to upload!'); });
Pretty long already! But we need to tackle the most important part: the ajax call. Right after appending ‘ajax’, add the following:
$.ajax({ url: $('#customizationForm').attr('action'), type: 'POST', data: data, cache: false, dataType: 'json', processData: false, contentType: false, success: function(data, textStatus, jqXHR) { }, error: function(jqXHR, textStatus, errorThrown) { } });
Explanation: two parts of this call deserve a better explanation; first, we are telling jQuery not to process data. Otherwise, the files array will be converted into a string, making it impossible to be further processed; then, we are setting contentType to false as well, to prevent the script to send over a normally encoded form (without files).
At this point, it’s worth testing out the script. It will inevitably break at some point, but you can see the upload being successful or not by refreshing the page when you see the call is complete. Make sure you keep an eye on the Network Tab in the browser’s console for this, to also troubleshoot any eventual error.
When it’s done, refresh the page. If everything went smoothly, the image will appear at the top of the file input!
Giving some feedback to the user
If we were the only ones using the site, we could as well jump to the next step. But since it’s rarely the case, let’s give our customers a bit of feedback on what’s up with files. In the “success” method of the ajax call, add the following:
if(typeof data.errors === 'undefined') { $.each(files, function(key, obj) { $('input[name="'+obj.name+'"]').addClass('filled'); previewFile($('input[name="'+obj.name+'"]'), obj.file); }); $('.uploadingfiles').text('Upload Complete!'); } else { $('.uploadingfiles').text('Error while uploading, please refresh the page and try again'); } $('.myoverlay').click(function(){$(this).remove()});
Explanation: first off, we must check that we don’t have any error coming back from the call. If we do, we replace our “uploading” text with the error string. If everything is ok (if(typeof data.errors === ‘undefined’)) we iterate through our files list, and add a preview above the list. Notice I am calling a function named previewFile, which we do not have yet!
You might have noticed I added a “filled” class as well to the input. Why? We will see this shortly, when amending tools.js.
Make sure you always add
$('.myoverlay').click(function(){$(this).remove()});
So that users can get rid of the overlay when needed.
Adding an image Preview
We are almost done! let’s create the previewFile function. Right before this:
$('#uploadTrigger').click(function(e) {
Add
function previewFile(target, file) { $('#uniform-'+target.attr('id')).before($('<img id="preview-'+target.attr('id')+'"/>')); var preview = $('#preview-'+target.attr('id')); var reader = new FileReader(); preview.attr('width', 64); reader.onloadend = function () { preview.attr('src', reader.result); } if (file) { reader.readAsDataURL(file); } else { preview.attr('src', ""); } }
Explanation: we are taking advantage of the FileReader class. First, we prepend an image tag to our input’s parent, making sure the preview box is no larger than 64 pixels. Then, we replace out image source after the reader has actually read the file, if it exists, passing in the one we got before, out of the original “files” array. It’s a bit confusing, but you can basically copy/paste to have it working. Here is the source: Image preview for ajax file uploads
As a finishing touch, we might want to handle failed ajax requests as well:
error: function(jqXHR, textStatus, errorThrown) { $('.uploadingfiles').text('ERRORS: ' + errorThrown); $('.myoverlay').click(function(){$(this).remove()}); }
Yet again, tools.js!
Back to our beloved tools.js! Here is where we left off:
function checkCustomizations() { var pattern = new RegExp(' ?filled ?'); if (typeof customizationFields != 'undefined') for (var i = 0; i < customizationFields.length; i++) { if (parseInt(customizationFields[i][1]) == 1 && ($('#' + customizationFields[i][0]).val() == '')) return false; } return true; }
Now, this will make it impossible to save our current files. In order to be able to do it, we need to modify the function as follows
function checkCustomizations() { var pattern = new RegExp(' ?filled ?'); if (typeof customizationFields != 'undefined') for (var i = 0; i < customizationFields.length; i++) { if (parseInt(customizationFields[i][1]) == 1 && $('#' + customizationFields[i][0]).val() == '' && !pattern.test($('#' + customizationFields[i][0]).attr('class'))) return false; } return true; }
Explanation: remember when we added the ‘filled’ class? Now we also want to make sure the class is not there, to return false. This way, the field will validate if it hass the ‘filled’ class!
Save and try it out, it should work!
The final code for product.js:
var files = new Array(); $('.customizationUploadLine').find('input[type="file"]').on('change', prepareUpload); // Grab the files and set them to our variable function prepareUpload(event) { files.push({'name' : event.target.name, 'file' :event.target.files[0]}); } function previewFile(target, file) { $('#uniform-'+target.attr('id')).before($('<img id="preview-'+target.attr('id')+'"/>')); var preview = $('#preview-'+target.attr('id')); var reader = new FileReader(); preview.attr('width', 64); reader.onloadend = function () { preview.attr('src', reader.result); } if (file) { reader.readAsDataURL(file); } else { preview.attr('src', ""); } } $('#uploadTrigger').click(function(e) { if(files.length > 0) { $('<div class="myoverlay"></div>').css({ 'position' : 'fixed', 'top' : 0, 'left' : 0, 'background' : 'black', 'background' : 'rgba(0,0,0,.5)', 'z-index' : 5999, 'width' : '100%', 'height' : '100%', 'cursor' : 'pointer' }).appendTo('body'); $('<div class="uploadingfiles">Your files are being uploaded...<img src="'+baseUri+'themes/default-bootstrap/img/ajax-loader.gif"></div>') .css({ 'position' : 'absolute', 'top' : '30%', 'left' : '50%', 'width' : '300px', 'margin-left' : '-150px', 'text-align' : 'center', 'padding' : '10px', 'background' : 'white' }) .appendTo('.myoverlay'); var data = new FormData(); $.each(files, function(key, obj) { data.append(obj.name, obj.file); }); data.append('submitCustomizedDatas', 1); data.append('ajax', 1); $.ajax({ url: $('#customizationForm').attr('action'), type: 'POST', data: data, cache: false, dataType: 'json', processData: false, contentType: false, success: function(data, textStatus, jqXHR) { if(typeof data.errors === 'undefined') { $.each(files, function(key, obj) { $('input[name="'+obj.name+'"]').addClass('filled'); previewFile($('input[name="'+obj.name+'"]'), obj.file); }); $('.uploadingfiles').text('Upload Complete!'); } else { $('.uploadingfiles').text('Error while uploading, please refresh the page and try again'); } $('.myoverlay').click(function(){$(this).remove()}); }, error: function(jqXHR, textStatus, errorThrown) { $('.uploadingfiles').text('ERRORS: ' + errorThrown); $('.myoverlay').click(function(){$(this).remove()}); } }); } // end checking files length else alert('Nothing to upload!'); });
Taking it a step further
Given that the previous works for you, we can try taking it a step further. If you really (really!) think your customers will never hit the upload button, you can automate the process and build it inside the ajax-cart as well. How? Opening ajax-cart.js, locate the previous code we added in part1:
if(addedFromProductPage && $('#customizationForm').length > 0) { ... }
Right before it, add:
$('#uploadTrigger').click();
Then, inside product.js, make sure you also add
async: false,
As property of the ajax call, otherwise the add to cart will run before the upload is completed!