Html - Using Buttons for selecting files to be uploaded

Well, you know the good old <input type="file" /> widget in html, the one used for selecting files to be uploaded on a web page, right.

The problem with this widget is its limited support for styling and scripting compared to other html tags (probably due to security concerns). You cannot style or specify text for the "Browse" button for this widget, for example.

In my case, I actually needed to use an ExtJs button for selecting files to be uploaded. I needed it to look exactly like an ExtJs button, yet pop-up a dialog for selecting file when the button is clicked. Once selected, I needed to show the selected file at another place in the UI. You can compare this to "Attach a file" option in Gmail, you click on the link to select a file, and when you select it, Gmail shows the selected file above the link and the link continues to be there for selecting another file, I needed something similar but using an ExtJs button instead (if you haven't noticed it, its interesting to note that the "Attach a file" button in Gmail is actually a Flash movie, try right-clicking on it and notice the context menu).

My initial instinct was to somehow trigger the "click" event on a "file" input field when an ExtJs button was clicked to show the file selection dialog (that's the only way in html/javascript you can ask browser to open file selection dialog, by clicking an <input type="file" /> field). However, I noticed that FireFox does not support this, and you cannot manually fire a "click" event on "file" field in FF.

Flash was never an option for me (there is a way you can do it in Flash, and that's why Gmail uses Flash for the purpose), because the same page needed to behave similarly in mobile device browsers also, which as you may know do not support Flash.

Now after some fiddling around, I thought of overriding the ExtJs button rendering and render a <input type="file" /> tag instead of the default <button /> tag for the ExtJs button. There was some success, but the ExtJs Button was not looking good across browsers. As I was searching for better ways to style the "file" input field, I stumbled upon this excellent article on quirksmode.org:
CSS2/DOM - Styleing an input type=file

I very much liked the idea there, which basically involves overlapping a transparent "file" input field over any other styled html content. Because the "file" field is transparent (opacity: 0), you see the underlying styled html. But because the "file" field is still present over the html, clicking html content actually triggers the click on the "file" field leading the browser to show the file selection dialog.

I immediately felt this was the trick I needed in my situation too. I would overlap a "file" field over an ExtJs button and the rest should be as simple as it gets. So, the only remaining concern now was to overlap the "file" field over an ExtJs button consistently across browsers.

You can see the result for the same below:

 

You can see examples for a normal ExtJs button, an ExtJs SplitButton as well as a plain html button above, all doing the same thing that I needed. Try clicking all the buttons and select a file on the dialog.

Here's the code for this functionality for the ExtJs buttons:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.override(Ext.Button, { actAsFileUpload: false, selectedFileOnce: function(e, t, o) { var el = this; var inputEl = Ext.fly(t); inputEl.removeClass('z-button-file-input'); inputEl.setStyle({ width: '', height: '' }); inputEl.purgeAllListeners(); t.parentNode.removeChild(t); el.fireEvent('fileselected', el, t); this.addNewInputEl(); }, syncInputElSize: function() { var el = this; var buttonEl = el.el.child('button'); var inputEl = el.wrap.child('input'); var width = buttonEl.getWidth(); var height = buttonEl.getHeight(); inputEl.setWidth(width + 5); inputEl.setHeight(height + 8); }, addNewInputEl: function() { var el = this; var buttonEl = el.el.child('button'); var inputEl = document.createElement('input'); inputEl.type = 'file'; inputEl = Ext.fly(inputEl); inputEl.addClass('z-button-file-input'); inputEl.on('change', el.selectedFileOnce, el, { single: true }); inputEl.on({ scope: el, mouseenter: function() { this.addClass(['x-btn-over', 'x-btn-focus']) }, mouseleave: function() { this.removeClass(['x-btn-over', 'x-btn-focus', 'x-btn-click']) }, mousedown: function() { this.addClass('x-btn-click') }, mouseup: function() { this.removeClass(['x-btn-over', 'x-btn-focus', 'x-btn-click']) } }); inputEl.insertBefore(el.el); this.syncInputElSize(); }, initComponent: Ext.Button.prototype.initComponent.createInterceptor(function() { this.addEvents('fileselected'); }), onRender: Ext.Button.prototype.onRender.createSequence(function(ct, position) { if (this.actAsFileUpload) { var el = this; el.wrap = el.el.wrap({ style: { position: 'relative'} }); el.el.setStyle({ position: 'absolute', top: '0px', left: '0px', zIndex: 1 }); el.addNewInputEl(); } }) });{/syntaxhighlighter}

In the above code, I have overriden the base Ext.Button class and added a config option "actAsFileUpload" together with some methods to it. When you actually instantize a Button/SplitButton and pass "true" for "actAsFileUpload", the onRender sequenced method wraps the default Button html in a <div> with its "position" set to "relative". Further the button itself is positioned absolutely inside this div. 

Next, an <input type"file" /> is instantized and placed transparently over the button. When you think you are clicking the button, its actually the "file" field which receives the click and the browser shows the dialog. When you select a file there, the onchange listener for the file field invokes the "fileselected" event on the button which you can handle to do whatever you want with the file input field. Here's what the above code does in "fileselected" event listener:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }new Ext.Button({ text: "Click me", renderTo: 'div1', actAsFileUpload: true, listeners: { fileselected: function(btn, t) { document.getElementById('fileDiv').appendChild(t); } } });{/syntaxhighlighter}

Cool, isn't it.. Now for the pure javascript button, here's the code that places a field field over it and handles file selection subsequently:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }function fileInputChanged() { var t = this; t.setAttribute('class', ''); t.style.position = ''; t.style.width = ''; t.style.height = ''; t.onchange = ''; t.parentNode.removeChild(t); document.getElementById('fileDiv').appendChild(t); addNewInputEl(document.getElementById('btn1')); } function addNewInputEl(button) { var inputEl = document.createElement('input'); inputEl.type = 'file'; inputEl.setAttribute('class', 'z-button-file-input'); inputEl.style.position = 'relative'; inputEl.style.width = button.offsetWidth; inputEl.style.height = button.offsetHeight; inputEl.onchange = fileInputChanged; button.parentNode.insertBefore(inputEl, button); } var btn = document.getElementById('btn1'); addNewInputEl(btn);{/syntaxhighlighter}

The logic is similat as for an ExtJs button, just that the absence of the utility DOM manipulation methods makes it less fun :-)

You will find the complete code for the above example attached below.

 

AttachmentSize
HTML icon html-upload-files-with-buttons.html4.85 KB

Comments

nice article sir..

Great article Rahul, I've been bannging my head with this pitfall for a long time. And this helped me a lot, eventually I was trying to work it out using ExtJs4 (i.e. v4.0.2) and this code seems broken there, although is working as it should with ExtJs3.2.1. 

I've simplified your example from above and I'm only using ExtJs button. Check it here: http://goo.gl/XYRK6 or in the attachment.

Basicly createInterceptor() at line 80 and createSequence() at line 91 are not allowed to be called in the same manner as before. I guess it has something to do with the prototype not been created in the same scope as the constructor.......I've been doing some hopeless trials to fix this, you can see my lines are commented out.

Do you have workaround on this? Thanks in advance!

rahul's picture

Hi, I was on a vacation and am just back. I might take upto a week to get back to you as I have plies of mails to clear up right now.

Nice work Rahul.

I'm trying to get a filefield button to do an over. Unfortunately, it doesn't work on ExtJS 4.1. The best solution would be to get a button to work as a filefield (exactly as you did for 3.4). Is there any way you can make this code work in 4.1?

Thanks.

rahul's picture

Hi Alex, unfortunately owing to my schedule, I won't be able to give it a try.

Which as you may know do not support Flash. - James Cullem