Fusioncube

The online journey of a technophile, by Steve Brownlee
Tuesday
Aug 16,2011

More of a personal post for future reference, but in case anyone else stumbles across this, let me know if you have a better way of doing this.

I’m writing an application with ExtJS 4 and when the user clicks on the close button a Window header, I simply want to close it and reset the state on the parent window. However, when the user clicks on the Save button, I want to perform some other actions and then close the child window and the state of the parent window’s controls will be something else.

My problem is that in the MVC architecture defined in ExtJS 4, I only had the child window’s close event on which I could listen – that event is fired when the user clicks on the header icon, and when I execute window.close() in the save method’s logic.

Anyway, the only way I could capture when the user clicked on the header close icon was to override the Ext.panel.Panel method initTools(). I simply copied the entire method from ext-debug-all.js and changed which method handles the header button click gesture. I then added the headerClose() method which is basically the close() method but with an added event being fired – headerclose.

Ext.define('MyApplication.Child.Window' ,{
   initTools : function() {
      ...
      if (me.closable) {
         me.addClsWithUI('closable');
         me.addTool({
            type: 'close',
            handler: Ext.Function.bind(me.headerClose, this, [])
         });
      }
      ...
   },

   headerClose: function() {
      if (this.fireEvent('beforeclose', this) !== false) {
         this.fireEvent('headerclose', this);
         this.doClose();
      }
   }
};

Now, in my controller I can handle that gesture specifically while handling the more general close event in a different way.

/*
 * Action handler for when the user clicks on the window close button
 * on the child window, while the general close event does nothing
 */
'childWindow': {
   headerclose: function() {
      this.resetParentState();
   },
   close : function() {
      // Just a stub to show nothing is done on close()
   }
},

'saveButton': {
   click : function(button) {
      doSomethingVeryCool();
      button.up('window').close();  // This will not reset the state of parent window
   }
}
Friday
Feb 25,2011

Did you know that you can use the table layout in a ExtJS Toolbar? I didn’t until a few days ago. Not only that, but they provide the ability to have a large icon representing a button with a menu, kinda like how the new version of Microsoft Office programs do it.

Brain? Open the floodgates.

I’m still working on redesigning an application at work while the project has spun, yet again, back to the requirements stage. Frustrating? A little, but it does give me time to try some cool things with the user interface and clean up some code.

Boring Toolbar

Anyway, here’s a nice, clean, but boring toolbar I have for this application. Not bad, but there’s just no pizzazz. It’s very corporate looking. I decided to try out the table format and use some icons from our stock library to add some of that pizzazz it’s missing.

image

Fancy Toolbar

Here’s how it looks with all the cool icons, and menu buttons. The only drawback is it takes up 60 more pixels of my vertical real estate in the application. Therefore, for the users who don’t like all the frilly colors, and fancy doodads, or simply dislike how much space it takes up, I provided a menu item in the Help menu to toggle between the two!

image

Some sanitized code to see how this is done. First, I added each icon that I wanted to use as a class in my application’s CSS library.

...
.icon-help {
    background-image:url('../images/icon-help.png') !important;
}

.icon-dollar {
    background-image:url('../images/icon-dollar.png') !important;
}
...

Then I set up an Ext.ButtonGroup with a table layout. In this sample code, I’m using three columns of icons. In the first column is a large icon button with a sub-menu. The second row contains the same format. The third row contains two smaller icons aligned vertically. This is accomplished by specifying rowspan : 2 on each of the larger icon button in the first two columns.

/*
 *  S A M P L E   M E N U
 */
var SampleButtonMenu = new Ext.ButtonGroup({
   title   : 'Sample',
   columns : 3,
   layout  : 'table',
   height  : 80,
   items   : [{
      text       : 'Hierarchy',
      iconCls    : 'icon-hierarchy',
      rowspan    : '2',
      scale      : 'medium',
      arrowAlign : 'bottom',
      iconAlign  : 'top',
      width      : 50,
      menu       : HierarchyMenu
   },{
      text       : 'Inclusion',
      iconCls    : 'icon-treatment',
      rowspan    : '2',
      scale      : 'medium',
      arrowAlign : 'bottom',
      iconAlign  : 'top',
      width      : 40,
      menu       : [{
         text: 'Sample Inclusion Button Menu',
         handler: function(){ document.location.href = 'index.cfm?sampleInclusion'; }
      }]
   },{
      iconCls : 'icon-route',
      tooltip: 'Route',
      handler: function(){ document.location.href = 'index.cfm?routeSetup'; }
   },{
      iconCls : 'icon-bandage',
      tooltip: 'Access Mapping',
      handler: function(){ document.location.href = 'index.cfm?accessMapping'; }
   }]
});

Using Cookies

Since this is a simple user preference and has no other impact on the application, I decided to store their choice in a cookie. Here’s the very simple code below. Now, the first thing you might notice is I’m refreshing the page when the user toggles between the two.

Unfortunately, I had no choice in this because I tried using Toolbar.removeAll(), then adding all the menu items, then Toolbar.render(‘element’), and then Toolbar.doLayout() and this worked fine if I toggled once. If I toggled more than once, ExtJS choked on properly rendering the sub-menus for the buttons or top-level menus. No idea why, so I’m stuck with reloading the page.

/*
 *  D E T E R M I N E   M E N U   S I Z E
 *
 *  This is stored in a cookie.  If the cookie doesn't exist, default to small menu.
 */
var MenuSize = Ext.util.Cookies.get('MenuSize');

if (MenuSize == null) {
   Ext.util.Cookies.set('MenuSize', 'small');
   MenuSize = 'small';
} 

/*
 *  H E L P   M E N U
 */
var AboutWindow;
var HelpButtonMenu = new Ext.ButtonGroup({
   id: 'LargeHelpMenu',
   title  : 'Help',
   layout : 'table',
   height : 80,
   items  : [{
      text       : 'Charm Help',
      iconCls    : 'icon-help',
      scale      : 'medium',
      arrowAlign : 'bottom',
      iconAlign  : 'top',
      width      : 50,
      menu       : [{
         text : 'Minimize Toolbar',
         handler: function(){
            Ext.util.Cookies.set('MenuSize', 'small');
            document.location.href = document.location.href;
         }
      },{
         text: 'About',
         handler: showAboutWindow
      }]
   }]
});

var HelpMenu = new Ext.menu.Menu({
   id: 'SmallHelpMenu',
   items: [{
      text : 'Maximize Toolbar',
      handler: function(){
         Ext.util.Cookies.set('MenuSize', 'large');
         document.location.href = document.location.href;
      }
   },{
      text: 'About',
      handler: showAboutWindow
   }]
});

Sencha ExtJS: Using bubbleEvents

Wednesday
Dec 22,2010

I can’t believe it took this long for this to click in my head. I’m sure I had seen the bubbleEvents() config option on ExtJS component documentation before, but my eyes just swept over it like when you’re looking for your kid in a crowd. the unimportant information immediately gets dismissed by your brain.

Well, today, I noticed an old bit of code in a Ext.Window object.  Here’s an example:

MappingDialog = function(){
   var dialog;
   return {
      init : function(){
         dialog = new Ext.Window({
            width:     200,
            height:    150,
            modal:     true,
            closable:  false,
            buttons: [
               { text:'Save', handler:function(){ dialog.fireEvent('saveMapping'); } },
               { text:'Cancel', handler:function(){ dialog.hide() } }
            ]
         });

         dialog.addListener('saveMapping', this.saveData, this);
      },
      saveData : function(){
         ... Save the data ...
      },
      showWindow : function(){ dialog.show(); },
      hideWindow : function(){ dialog.hide(); }
   }
};

Notice how I’m referencing the dialog object inside the Save and Cancel button handlers. That’s ugly, but at the time I wrote it I didn’t know how to do it differently. Today, however, I focused in on that dialog.fireEvent() statement and thought to myself, “Self, there has to be a way to bubble the event of a button up to its parent window, so I can avoid having direct references to the Window inside the Button – which is inside the Window.”

Off to the ExtJS API Docs and I immediately notice the bubbleEvents config option which, like I said, I’d glazed over hundreds of times in the past. remember? Well, using that, I now bubble any event up to its parent container which can handle it appropriately. Much more efficient code from a memory perspective and from a maintenance perspective as well, as renaming the Window won’t require a global search/replace.

MappingDialog = function(){
   return {
      init : function(){
         dialog = new Ext.Window({
            width:     200,
            height:    150,
            modal:     true,
            closable:  false,
            <strong>bubbleEvents: ['saveMapping'],</strong>
            buttons: [{
               text:'Save',
               <strong>bubbleEvents: ['saveMapping'],</strong>
               handler:function(){ this.fireEvent('saveMapping'); }
            },{
               text:'Cancel',
               <strong>bubbleEvents: ['closeWindow'],</strong>
               handler:function(){ this.fireEvent('closeWindow'); }
            ],
            listeners: {
               saveMapping : function() { this.fireEvent('saveMapping'); },
               closeWindow : function() { this.close(); }
            }
         });

         this.addListener('saveMapping', this.saveData, this);
      },
      saveData : function(){
         ... Save the data ...
      },
      showWindow : function(){ dialog.show(); },
      hideWindow : function(){ dialog.hide(); }
   }
};
Tuesday
Jan 8,2008

These three Javascript libraries are at the core of how I build fun, well-organized, and easy to maintain web applications. Each one serves a different purpose:

  • jQuery – The best at DOM selection, manipulation and searching
  • Ext – Makes all my little widgets look great and fun for the users
  • ajaxCFC – Handles calls to my business logic and serialization of data

I’ll start off with a nice example of how these three libraries play well together and can make an otherwise complicated action very simple and easy to write.

This code has three pieces of functionality. I’m using the jQuery Datepicker on my page, and when the user selects a date from the calendar, I want the database to immediately be updated with the date chosen. If the method fails, throw up a nice message box with a failure message and ask the user to try again.

// Use jQuery to bind the dataSelected event of the jQuery DatePicker
$('#requestDueDate').bind('dateSelected',function(e, selectedDate, $td){
        // Use ajaxCFC to call the updateRequestDates() function of the Request component
	$.AjaxCFC({
		url: "model/ajax/request/Request.cfc",
		useDefaultErrorHandler: false,
		method: "updateRequestDates",
		data: { 'request_hdr_seq_no': request_hdr_seq_no,
			'request_due_date': selectedDate.asString(),
			'test_completion_date':$("#testCompletionDate").val() },
		timeout:30000,
		success: submitRequestCheck,
		error: function(results) {
                        // If the call fails, throw up an Ext message box to alert the user
			Ext.MessageBox.alert('Update Failed', "Failed to update the request due date. Please try again.");
			$('input#testCompletionDate').val("");
		}
	});
});

All I was required to do was write 14 lines of code. Now, I can’t even imagine having to write this code without the use of the Big 3.

Now for one of my favorite features of this site (it’s actually a boring feature, but how I implemented it is cool).
Notes Accordion Dialog II

This is just a simple note feature where users can record any pertinent information about the work they are performing. I’m using the Ext Accordion Widget to display all past notes. You can see how the user can expand and contract each note item – not really needed, but never hurts to wow the users a little.

This particular page was written to operate as a true application. Any changes to the UI are immediately written to the database via AJAX calls. The UI is then updated when the operation completes, and the user can continue working while several tasks are being executed. No page reloads at all.

So when they add a note, it must be inserted into the database, and the Accordian object must have a new panel added to it, with the new text inserted inside.

I’ll admit, it stretched all my capabilities and imagination for Javascript programming. All in all, it took over 3 days of trying different things, and research, to get it to work.

First, I had to include the required features of the libraries.

<script type="text/javascript" src="js/jquery.AjaxCFC.js"></script>
<script type="text/javascript" src="js/json.js"></script>

<script type="text/javascript" src="js/ext-buttons.js"></script>
<script type="text/javascript" src="js/ext-layout.js"></script>
<script type="text/javascript" src="js/Ext.ux.InfoPanel.js"></script>
<script type="text/javascript" src="js/Ext.ux.Accordion.js"></script>

*This adds up to 300k of jQuery/Ext Javascript libraries.

Then, I build the <div> element that will be used to display the dialog and the accordion panels

<div id="NoteModalDialog">
	<div class="x-dlg-hd">Request Notes</div>
	<div class="x-dlg-bd">
       	<div class="x-dlg-tab" title="Notes">
			<div id="RequestNoteForm"></div>
			<div style="font-size:1.1em; font-weight:800;">Previous Notes</div>
			<div id="acc-ct" style="width:100%; height:200px">
				<cfloop query="requestNotesQuery">
					<cfoutput>
					<div id="panel-#currentRow#">
						<div>
							#first_name# #last_name# - #setup_date#:
						</div>
						<div>
							<div class="text-content">#note_txt#</div>
							<div style="border-bottom:5px solid white;"></div>
						</div>
					</div>
					</cfoutput>
				</cfloop>
			</div>
		</div>
	</div>
</div>

Then I instantiate the accordion object and build all the panels with previous notes

$(document).ready(function(){
	// Create accordion to hold previous notes in note popup
	var accordion = new Ext.ux.Accordion('acc-ct', { height:400,independent:true});

	// Create a panel for each previous note
	<cfloop query="requestNotesQuery"><cfoutput>
		var panel#currentRow# = accordion.add(new Ext.ux.InfoPanel('panel-#currentRow#', {collapsed:false}));
	</cfoutput></cfloop>
}

Then, of course, I need my wonderful Javascript object that handles building the dialog, saving the note, and loading of previous notes back into the UI/DOM.

It first builds a dynamic Ext form (which includes validation).

var noteForm = new Ext.form.Form({ labelWidth:25 });
var noteText = new Ext.form.TextArea({
	fieldLabel: 'Note',
	name: 'noteText',
	width:375,
	allowBlank:false,
	grow:true,
	growMax:200,
	emptyText:"Enter your request notes here..."
});
noteForm.add(noteText);

Then using ajaxCFC, I insert the data into database

$.AjaxCFC({
	url: "model/ajax/note/Note.cfc",
	useDefaultErrorHandler: false,
	method: "add",
	async:false,
	data: { 'seq_no': request_hdr_seq_no, 'note_type':'REQUESTHDR', 'note_txt': noteText.getValue() },
	timeout:30000,
	success: function(results) {
		// Reset the note field to blank
		noteText.setRawValue("");

		{{ This nested ajaxCFC call code shown below }}
	},
	error: function(results) {
		Ext.MessageBox.alert('Save Failed', results.responseText);
	}
});

Upon success of the insertion of the data, update the UI by adding a panel to the Accordion object with the new note inside.

// Return all notes from the database for this request
$.AjaxCFC({
	url: "model/ajax/request/Request.cfc",
	useDefaultErrorHandler: false,
	method: "getAllRequestNotes",
	async:false,
	data: { 'request_hdr_seq_no': request_hdr_seq_no },
	timeout:30000,
	success: function(results) {
		// Empty out the current accordion object and re-populate it with a panel for each note
		var panel = new Object();
		var accordion = new Ext.ux.Accordion('acc-ct', { height:'400', independent:true });
		$("#acc-ct").empty();

		for(i=0; i<results.NOTECOUNT;i++) {
			$("#acc-ct").append('<div id="panel-' + i + '"><div>' + results.PANELS[i].FIRST_NAME + ' ' + results.PANELS[i].LAST_NAME + ' - ' +    results.PANELS[i].SETUP_DATE + '</div><div><div class="text-content">' + results.PANELS[i].NOTE_TXT + '</div></div></div>');
			panel[i] = accordion.add(new Ext.ux.InfoPanel('panel-'+i, {collapsed:false}));
		}
	},
	error: function(results) {
		Ext.MessageBox.alert('UI Update Failed', "Failed to update Notes popup with latest note.");
	}
});

I put it all together, and I get a self-documenting, elegant Javascript object that handles user notes.

// Create the object that will be used for the Request Notes popup
var requestNotesEdit = function() {
	var dialog;
	var noteForm = new Ext.form.Form({ labelWidth:25 });
	var noteText = new Ext.form.TextArea({
		fieldLabel: 'Note',
		name: 'noteText',
		width:375,
		allowBlank:false,
		grow:true,
		growMax:200,
		emptyText:"Enter your request notes here..."
	});
	noteForm.add(noteText);

	return {
		init : function() { this.buildDialog(); },
		saveData : function() {
			if (noteForm.isValid()) {

				$.AjaxCFC({
					url: "model/ajax/note/Note.cfc",
					useDefaultErrorHandler: false,
					method: "add",
					async:false,
					data: { 'seq_no': request_hdr_seq_no, 'note_type':'REQUESTHDR', 'note_txt': noteText.getValue() },
					timeout:30000,
					success: function(results) {
						// Reset the note field to blank
						noteText.setRawValue("");

						// Return all notes from the database for this request
						$.AjaxCFC({
							url: "model/ajax/request/Request.cfc",
							useDefaultErrorHandler: false,
							method: "getAllRequestNotes",
							async:false,
							data: { 'request_hdr_seq_no': request_hdr_seq_no },
							timeout:30000,
							success: function(results) {

								// Empty out the current accordion object and re-populate it with a panel for each note
								var panel = new Object();
								var accordion = new Ext.ux.Accordion('acc-ct', { height:'400', independent:true });
								$("#acc-ct").empty();

								for(i=0; i<results.NOTECOUNT;i++) {
									$("#acc-ct").append('<div id="panel-' + i + '"><div>' + results.PANELS[i].FIRST_NAME + ' ' + results.PANELS[i].LAST_NAME + ' - ' +    results.PANELS[i].SETUP_DATE + '</div><div><div class="text-content">' + results.PANELS[i].NOTE_TXT + '</div></div></div>');
									panel[i] = accordion.add(new Ext.ux.InfoPanel('panel-'+i, {collapsed:false}));
								}
							},
							error: function(results) {
								Ext.MessageBox.alert('UI Update Failed', "Failed to update Notes popup with latest note.");
							}
						});

					},
					error: function(results) {
						Ext.MessageBox.alert('Save Failed', results.responseText);
					}
				});

				dialog.hide();
			}else{
				Ext.Msg.alert('Field Required', 'Please enter in some note text before submitting.');
			}
		},
		buildDialog : function() {
			noteForm.render('RequestNoteForm');
			dialog = new Ext.BasicDialog("NoteModalDialog", {
				modal:true,
			    width:450,
			    height:350,
			    shadow:true,
			    minWidth:400,
			    minHeight:300,
			    animateTarget:'requestNotes'
			});
			dialog.addKeyListener(27, this.hideWindow, this);
	        dialog.addButton('Save', this.saveData, this);
	        dialog.addButton('Cancel', this.hideWindow, this);
		},
		showWindow : function() { dialog.show(); },
		hideWindow : function() { dialog.hide(); }
	};
};

About Steve

I am a technologist, and have been ever since 1980 when I got my very first TRS-80 and programmed it to do my math homework. I love to share the gift of technology with others and show them the wonderful things it can do for them, and how they should not fear it, but embrace it.

Latest Tweets

  • Ok... stayed up way too late trying out website designs for my wife's new nonprofit. The kids will be getting me u... — http://t.co/QrKh5iBI
  • Am I the only one who likes Google’s new privacy policy? http://t.co/qwcym5wH
  • All that time wasted learning the .NET framework - Fusioncube - http://t.co/krANoWmg
  • @marcesher libraries like Less only do what you tell them. You can make a mixin to do that, but it doesn't assume anything (which is good)
  • Circus about to start. Girls are so excited they can't stand it!!!! (with Sabrina and Tessa at @BrdgstoneArena) [pic] — http://t.co/PXwi5emj

Subscribe

Entries (RSS)
Comments (RSS)