Okay, now this post might come across as a bit of a rant but could be of real use to people who are looking to implement a custom file upload widget. Lets face it, the default file input container provided by HTML is not what we’d describe as eye-candy and to top it off each browser lends its own rendering to the input tag. Heterogeneous user experience is a nightmare for designers/front-end developers. So we often rely on third party javascript to do the magic for us. However that may not always be the answer to your problems depending on the front-end framework/architecture being used in your project. I experienced this sort of problem recently while working with Angular Js as the front-end framework. I used dwr (Direct Web Remoting) module for actually handling the uploads. Relying on a third party module as an upload widget was just not working for sequential file uploads, so I applied for a more unconventional solution.
Preface:
My page consists of divs which loop and repeat the contents of my model:
<div ng-repeat=”item in itemlist”>
<form method=“post” action=“#” enctype=“multipart/form-data”>
<input id=”fileprefix{{item.itemId}}” type=”file”>
<button type=”button” ng-click=”controllerFileUpload(item,’fileprefix’+item.itemId)”>
Upload
</button>
</form>
</div>
Each div will have an input field containing an unique id and the button passes the model “item” and the file contents from the input field with the unique id.
It was pretty difficult use third party module for this use case since most of them use jquery to identify the input tag id, which of course, was provided dynamically by Angular. And so I ventured to create my own widget:
<div ng-repeat=”item in itemlist”>
<form method=“post” action=“#” enctype=“multipart/form-data”>
<input id=”fileprefix{{item.itemId}}” type=”file” onclick=”updatefield()” style=”display: none;”>
<div class=”input-append”>
<input id=filenamecontainer{{item.itemId}} type=”text” placeholder=”Choose File” disabled>
<a class=”btn btn-white” onclick=”copycarry()”>Browse Files</a>
<button type=”button” ng-click=”controllerFileUpload(item,’fileprefix’+item.itemId)”>
Upload File
</button>
<button type=”button” onclick=”resetval()”>
Clear selection
</button>
</div>
</form>
</div>
What we have here is two input fields: one that is actually used to store the file value is hidden from view using “display: none” and the second input field is used to display file name to the user. The output of this markup looks like:
In my case I’ve used font-awesome icons instead of text for the buttons, but to each his own. Now pay attention to the onclick events associated with the buttons and input fields. Before I delve into their code, notice that three of those fields have the normal javascript “onclick” attribute and the “Upload File” button has Angular Js “ng-click” attribute. This smells of complication, but let me justify myself here: Angular Js has access to a lot of Jquery tools with the presence of Jquery in your markup and if it isn’t included in your markup Angular has a built-in JQ-lite toolbox that gives it a lot of Jquery like functionality, but the problem is that it isn’t as convenient manipulating the DOM through Angular as it is with Jquery. You get more tools in Jquery and if you want DOM manipulation from Angular you’ll have to manually call “$apply” service which could lead to complication. So I opted for the best of both worlds and made use of Jquery for the DOM manipulation for styling and Angular Js for file upload. Take a look:
<script type=”text/javascript”>
function resetval(){
$(‘input[id^=”fileprefix“]’).filter(function(){
return /^fileprefixd+$/.test(this.id);
}).val(“”);
$(‘input[id^=”filenamecontainer“]’).filter(function(){
return /^filenamecontainerd+$/.test(this.id);
}).val(“”);
};
</script>
——————————————————————————————————————————–
<script type=”text/javascript”>
function copycarry(){
$(‘input[id^=”fileprefix“]’).filter(function(){
return /^fileprefixd+$/.test(this.id);
}).click();
};
</script>
——————————————————————————————————————————–
<script type=”text/javascript”>
function updatefield(){
$(‘input[id^=”fileprefix“]’).filter(function(){
return /^fileprefixd+$/.test(this.id);
}).change(function() {
$(‘input[id^=”filenamecontainer“]’).filter(function(){
return /^filenamecontainerd+$/.test(this.id);
}).val($(this).val());
});
};
</script>
The first function “resetval( )” is pretty self explanatory, it simply finds the element with id matching the regular expression “fileprefix” and “filenamecontainer” followed by a number and sets the value of that field to empty. The regular expression matching is important here because the number following the prefix in provided dynamically by Angular Js. So this way I was able to consume the dynamic nature of the id attributes.
The second function “copycarry( )” carries over the click action from the “Browse Files” button to the input field that will actually hold the file. The input file is hidden from view, but this function actually invokes the input field to open the file browser by emulating the click event.
The hidden input field also has “onclick” action attached to it. The “updatefield( )” function copies the file name value from the hidden input of type file to the visible input of type text which displays the file name to the user.
The function associated to the “Upload File” button is handled by Angular Js with the ngClick directive and the function is written into the controller which will retrieve the file from the hidden input field.
Thus, with what seems like a lot of patch work I was able to get it to work, but it does work smoothly and allows styling by simple twitter-bootstrap classes and without building any complicated custom directives in Angular Js to manipulate the DOM, we have taught a new trick to plain old HTML.