Once in a while, you pull of something quite spectacular. Loading Acrobat Reader inside an Ext.Window is such a feat. It’s the cleanest solution I could come up with that enables a single Rails action to handle the entire PDF creation process without a redirect, keeping a session, or using a GET request. Of course, it’ll work with any server side language or framework.
Assuming you’re following best practices for developing an ExtJS appplication, let’s say you have a nice button that triggers the report generation and Ext.Window creation:
liaison.ux.button.QuickReportDefault = Ext.extend(Ext.Button, { text:'Report', tooltip:'Quick Report', iconCls:'icon-lightning', fileType:'pdf', initComponent:function() { this.plugins = [new liaison.ux.plugin.QuickReport({fileType:this.fileType})]; liaison.ux.button.QuickReportDefault.superclass.initComponent.apply(this, arguments); } }); Ext.reg('quickreport-html-button', liaison.ux.button.QuickReportDefault);
No surprises. Ext.Button is extended, configured, then the resultant class registered as an xtype for use elsewhere in a call to Ext.Toolbar.
In good fashion, a plugin actually provides the necessary functionality.
liaison.ux.plugin.QuickReport = Ext.extend(Object, { fileType:'html', constructor:function(cfg) { Ext.apply(this, cfg); },
I extend Object so I needn’t bother with a contructor function, although I do define a constructor to consume the options passed to the plugin when I instantiate it.
init:function(b) { b.handler = this.generateReport; b.scope = this; },
All Ext plugins have an init function that is called during the initialization lifecycle of an Ext.Component. The only argument is the object including the plugin. Rubyists might recognize this as self.included style functionality. Above I configure the scope and handler for my button inside the plugin itself.
Finally, we arrive at the heart of the matter.
generateReport:function() { ... var win = new Ext.Window({ width:750, height:500, autoScroll:false, html:'', modal:true, maximizable:true, cls:'x-window-body-report', title:String.format('{0} Quick Report', grid.model.plural_title) }); win.on('close', function() { if(Ext.isIE) { win.body.dom.firstChild.src = "javascript:false"; } }, win);
A regular Ext.Window. Nothing special so far. It’s created empty. While you can instead use {tag:’iframe’}, I found Firefox would fail to successfully POST to the IFRAME when I allowed Ext to create it that way.
Therefore, let us create it manually. doFormUpload() is an excellent reference method in the Ext source code itself, although that IFRAME is designed to be hidden for form uploads.
// Magic from doFormUpload() var id = Ext.id(); var frame = document.createElement('iframe'); frame.id = id; frame.name = id; frame.frameBorder = '0'; frame.width = '100%'; frame.height = '100%'; frame.src = Ext.isIE ? Ext.SSL_SECURE_URL : "javascript:;"; win.show(); win.body.appendChild(frame); // Seems to be workaround for IE having name readonly. if(Ext.isIE) { document.frames[id].name = id; }
The magic above simply builds the IFRAME, ensures it has no borders, completely fills an Ext.Window at all times, and ensures that IE will POST to the IFRAME.
var form = new Ext.FormPanel({ url:String.format('{0}/report.{1}', grid.model.path(), this.fileType), renderTo:Ext.getBody(), standardSubmit:true, method:'POST', defaultType:'hidden', items:[].concat(formItems) }); form.getForm().el.dom.action = form.url; form.getForm().el.dom.target = id;
A BasicForm is created, but not assigned to the DOM, to handle the actual POST of the data sent to the backend for PDF building. formItems is created earlier in an application specific way and holds the values being transmitted to the backend. The dom.target is the magic that ensures the browser POSTs to the IFRAME itself.
Finally, because of a bug in Firefox since before 1.0, a loading message is displayed in every browser exception Firefox. Another approach for a load mask is probably necessary for Firefox, although I haven’t explored it in detail.
// http://extjs.com/forum/showthread.php?t=31461 // display/none seems to force a reload of Adobe Acrobat Reader in all FF <3.1 if(!Ext.isGecko) { var mask = new Ext.LoadMask(win.id, {msg:"Loading..."}); mask.show(); } Ext.EventManager.on(frame, 'load', function() { if(mask !== undefined) { mask.hide(); } form.destroy(); }); Ext.emptyFn.defer(200); // frame on ready? form.getForm().submit(); } });
On the Rails side, the controller code is quite simple. The PDF creation is handled by a Ruby gem called Ruport. Some of the controller bits are from using the resource_controller Rails plugin.
def report options = {} options[:collection] = collection options[:columns] = params[:columns] options[:headers] = end_of_association_chain.get_column_model_headers unless end_of_association_chain.respond_to? :report_table # Some kind of failure mode, possibly a nice failure doc into iframe. render :text => '' return end respond_to do |type| type.html do render :text => QuickReport.render(:html, options), :layout => 'report' end type.pdf do o = { :type => "application/pdf; header=present", :disposition => "inline; filename=#{end_of_association_chain.to_s.underscore}_report.pdf" } send_data QuickReport.render(:pdf, options), o end end end