3.5. The Code

Now that we've looked at the markup and the style sheet involved, we can move on to the actual code. Let's begin by looking at the DAO class, which is in a sense a stand-alone entity to the extent that you could rewrite the entire application and still reuse this class with little or no change.

3.5.1. The DAO Class

Next we move on to the DAO class, contained within the aptly named DAO.js file. This class presents the API to the rest of the application through which all access to the underlying Gears database will be made. This gives us the possibility of storing the data in some other fashion later, perhaps on a server, without changing the application code, which is one of the primary benefits of the DAO pattern.

Let's begin by getting a bird's-eye view of this class via Figure 3-7, a UML class diagram of it.

Figure 3.7. UML class diagram of the DAO class

First we see that there are two public fields:

DAO.TASK_STATUS_ACTIVE = "active";
DAO.TASK_STATUS_COMPLETE = "complete";

These are pseudo-constants that define the value that a task will have when it is active and when it is complete. Since JavaScript doesn't have the concept of a constant like most languages do, the best we can do is name them in a fashion that tries to indicate they are constants. There's a fairly standard way of doing that: all uppercase with underscores between words. This doesn't stop someone from changing the value of these fields, but by general convention most programmers will know they probably shouldn't just by looking at the name.

Next we find a private variable named databaseName:

var databaseName = "OrganizerExt";

This variable is used in the rest of the code to define the name of the database that Gears will store for us. This variable is optional since Gears will create a default name if you omit it, but it's cleaner to explicitly name something that makes sense. This value isn't needed outside the class; hence it's private to avoid any other code mistakenly changing it and breaking the application.

Following that is the definition of a couple of string variables:

var sqlCreateNotesTable = "CREATE TABLE IF NOT EXISTS notes (" +
  "id INT, category TEXT, content TEXT" +
")"
var sqlCreateNote =
  "INSERT INTO notes (id, category, content) " +
  "VALUES (?, ?, ?)";
var sqlRetrieveNotes = "SELECT * FROM notes";
var sqlDeleteNote = "DELETE FROM notes WHERE id=?";

This code defines some SQL statements related to notes, starting with a table creation statement. As you can see, it's a perfectly standard statement that creates the table if it doesn't yet exist (and does nothing if it does exist).

Following that is an SQL statement to create a new note. As you can see, dynamic parameters are present in the form of question mark placeholders. The actual values will be bound to these placeholders later when the statement is executed.

After that is the simple SQL statement to retrieve all notes. As it turns out, this is the only retrieval operation we'll need in this application, so it really is as simple as that.

Finally, there is an SQL statement used to delete a note. All this takes is an ID value for the note to delete, again using dynamic parameters.

THE U IN CRUD

If you've never heard the term CRUD before, now you have! CRUD stands for Create, Retrieve, Update, and Delete. These are the four basic operations that most database-driven applications need, and CRUD is a very common term in programming circles. It's also kind of fun to say, especially in place of more vulgar... er... vulgarities!

So, we can see here that there is a create SQL statement, a retrieve statement, and a delete statement for notes, but no update statement. The way I decided to code this application means that updating an item isn't necessary, at least in the case of notes, contacts, and appointments (tasks are a different story, as we'll see next).


In the interest of saving some space here I am not going to show the SQL statements for contacts, appointments, and tasks because they are, by and large, no different than what we just looked at, just with some different fields. Otherwise, they are the same, and there is a set of four SQL statements for contacts and appointments as well.

For tasks, however, there are five because there is an update query for them:

var sqlUpdateTask = "UPDATE tasks SET category=?, status=?, content=? " +
  "WHERE id=?";

Because a task can be updated in the sense that it can be marked as having been completed, we need such an update query. It works just the same as any of the others; it's just a slightly different query.

Before we continue looking at the code, I thought it would be a good idea to take a glance at the structure of each of the four tables (notes, tasks, contacts, and appointments). Seeing a slightly more graphical representation helps, so Figure 3-8 shows just such a representation of the contacts table.

Figure 3.8. Table structure of the contacts table

A grand total of ten fields are present for each contact, all of them of type text, except for the ID. Pretty straightforward, I suspect.

In Figure 3-9 you can see the corresponding diagram of the appointments table.

Figure 3.9. Table structure of the appointments table

There isn't as much information to store for an appointment, so six fields are all we need.

Next up is the notes table, with three fields, as shown in Figure 3-10.

Figure 3.10. Table structure of the notes table

To round things out, in Figure 3-11 is the same diagram for the tasks table. This is similar to the notes table, with the addition of the status field.

Figure 3.11. Table structure of the tasks table

Now, getting back to the code, we encounter our first method: init(). This method is responsible for some basic setup:

this.init = function() {

  var initReturn = true;
  if (!window.google || !google.gears) {
    initReturn = false;
  }

  var db = google.gears.factory.create("beta.database");
  db.open(databaseName);
  db.execute(sqlCreateNotesTable);
  db.execute(sqlCreateTasksTable);
  db.execute(sqlCreateContactsTable);
  db.execute(sqlCreateAppointmentsTable);
  db.close();

  return initReturn;

}

The first thing it does is ensure that Gears is installed and available. This will be the case if there is a google attribute on the window object and if there is a gears attribute on that google object. If either of those conditions isn't met, then the variable initReturn is set to false, which will be the variable returned from this method (we optimistically default its value to true in anticipation of no problems with Gears).

The next step is to ensure we have the tables we need. This is done by creating an instance of the beta.database object via a call to google.gears.factory.create(), as we've previously seen in Chapter 2. Then we open the database by name (using that private databaseName variable we saw earlier). After that we execute each of the four table creation SQL statements, one each for notes, tasks, contacts, and appointments. Recall that these statements will only have an effect when the tables don't already exist. Finally, we close the database (which is optional, but is good style nonetheless) and return that initReturn variable so the caller knows whether the underlying database is good to go.

Now that the database is initialized, we can go ahead and create items, delete them, and so on. In celebration of that, let's look at the next method, the createNote() method:

this.createNote = function(inNoteDesc) {

  if (inNoteDesc && inNoteDesc.id && inNoteDesc.category &&
    inNoteDesc.content) {
    var db = google.gears.factory.create("beta.database");
    db.open(databaseName);
    db.execute(sqlCreateNote, [
      parseInt(inNoteDesc.id), inNoteDesc.category, inNoteDesc.content
    ]);
    db.close();
  }

}

The inNoteDesc argument is an object that contains fields where the data for a note is stored. So, the first thing that's done is a check to ensure that we got an object for inNoteDesc (it's not null, in other words) and that the fields that are absolutely required for a note to be stored are not null either. In the case of a note, all of them are required, but that's not the case for other types of items. Once we do that verification, we again open the database and simply execute the sqlCreateNote query. Note the second argument to the db.execute() method: an array of data that will be inserted in place of those question mark placeholders we saw earlier. Gears will take care of properly escaping the inserted data, so this is a safe way to create a final SQL statement that avoids various hacking exploits that would otherwise be possible.

Now that we know how to create a note, seeing how to retrieve notes is the next logical step. Here's the code for that:

this.retrieveNotes = function() {

  var db = google.gears.factory.create("beta.database");
  db.open(databaseName);
  var rs = db.execute(sqlRetrieveNotes);

var results = [ ];
  while (rs.isValidRow()) {
    results.push({
      id : rs.fieldByName("id"),
      category : rs.fieldByName("category"),
      content : rs.fieldByName("content")
    });
    rs.next();
  }
  rs.close();
  db.close();

  return results;

}

So once again the database is opened, and the sqlRetrieveNotes query is executed. From this we have a ResultSet object, so we begin to iterate over that. This is done by continually checking to see if rs.isValidRow() returns true, which indicates we have another row of data to process. For each row, we create an object consisting of three properties: id, category, and content. These are the data stored for each note. The values of these attributes are pulled from the row of data using the rs.fieldByName() method, which simply gets the value of the named field from the row. This created object is pushed into the array created before the iteration began. Finally, the ResultSet and database are closed and a simple array of objects is returned. Note that the array could be empty, but null would never be returned from this method, which makes writing code that uses this method a little cleaner since there is no null checking to be done.

NOTE

You may wonder why I didn't simply return the ResultSet to the caller. This would have worked, with some changes to the calling code, but the reason for not doing that is because it creates a "leaky abstraction." In other words, this DAO class is currently the only code in the application that knows we're working with Gears. If we return the ResultSet, which is a Gears-supplied class, the rest of the application has to "know about" Gears as well. Transferring the data to a simple array of simple objects means the application is abstracted from the underlying data store, which allows us to change to a different store down the road (imagine if this method actually made an Ajax request to a server to get the data instead).

Deleting a note is much the same, although it takes even less code:

this.deleteNote = function(inNoteDesc) {

  if (inNoteDesc && inNoteDesc.id) {
    var db = google.gears.factory.create("beta.database");
    db.open(databaseName);
    db.execute(sqlDeleteNote, [ inNoteDesc.id ]);
    db.close();
  }

}

To start with we have another quick check of inNoteDesc, ensuring it's not null and that there is an id specified on it. After that it's a simple execution of the sqlDeleteNote query, dynamically inserting the id value, and that's that!

At this point you've seen how create, retrieve, and delete works for notes. For contacts, appointments, and tasks, the code is virtually identical. The only differences are the SQL queries executed and the fields referenced. Therefore, we won't look at the methods for those items here, but I encourage you to have a look at the code yourself.

There is only one thing left to look at: the updateTask() method, which is used to mark a task as complete:

this.updateTask = function(inTaskDesc) {

  if (inTaskDesc && inTaskDesc.id && inTaskDesc.category &&
    inTaskDesc.status && inTaskDesc.content) {
    var db = google.gears.factory.create("beta.database");
    db.open(databaseName);
    db.execute(sqlUpdateTask, [
      inTaskDesc.category, inTaskDesc.status, inTaskDesc.content,
      inTaskDesc.id
    ]);
    db.close();
  }

}

There should by this point be little, if any, surprises. There is more verification this time around because there are a few more required fields. In fact, it's all of them, because when updating a task the code makes no effort to determine what fields changed—it simply writes out the values for all of them. Otherwise, this method is no different than what you've seen before.

3.5.2. The OrganizerExt Class

The OrganizerExt class is the heart and soul of the application; it's where all the best parts are! It's also a fairly lengthy piece of code, although as you'll see, much of it isn't what most people consider "code" per se—it's more configuration-type code.

Let's start by looking at a UML class diagram of OrganizerExt, as shown in Figure 3-12.

Figure 3.12. UML class diagram of the OrganizerExt class

There's certainly a fair bit there, but as I've done before I'm going to cut some of it out of our discussion on the grounds that what we will look at basically gives you the picture for the pieces I skip as well. As always, though, I encourage you to look at the complete code in the book's source code, if for no other reason than to keep me honest!

3.5.2.1. Class Fields

Let's begin by looking at the fields that are part of this class, beginning with currentCategory:

this.currentCategory = "appointments";

This field, which is public, is used when the user clicks the View Mode toolbar button. It is necessary to know which category of items is currently being shown to properly switch the view mode, and while it likely would have been possible to interrogate the accordion itself to determine what the selected pane is, how to do that wasn't readily apparent to me. More importantly, this approach offers greater efficiency.

Next we encounter a series of four public fields:

this.notesStore = null;
this.tasksStore = null;
this.contactsStore = null;
this.appointmentsStore = null;

These hold references to the data stores we'll be creating, one for each category. We'll see how the stores are created and manipulated in fairly short order, but for now let's move on to another group of four public fields:

this.NoteRecord = null;
this.TaskRecord = null;
this.ContactRecord = null;
this.AppointmentRecord = null;

These hold references to the Record classes we'll create that describe a type of item. The Record classes describe what fields a note Record in a data store has, for example. Note that these variables, as well as the previous data store variables, are all public because they will need to be accessible outside the scope of this class, as we'll see later.

The next field is categoryList:

this.categoryList = [
  "Competition", "Family", "Favorites", "Gifts", "Goals/Objectives",
  "Holiday", "Home", "Hot Contacts", "Ideas", "International", "Key Custom",
  "Medical", "Meeting", "Miscellaneous", "Other", "Personal", "Phone Calls",
  "Status", "Strategies", "Suppliers", "Time And Expenses", "VIP", "Waiting",
  "Work"
];

This too is public because it will be needed outside the scope of the class. Its purpose is to provide the list of categories under which an item can be saved. The items will be used to populate the combo boxes on the various create forms, as well as in the Accordion panes for filtering items.

3.5.2.2. The Initialization Code

Now that we've looked at the fields of the class, we can move right into the executable code. The first method we encounter is init(), which you'll recall from looking at index.htm is called when the DOM is loaded:

this.init = function() {

  new Ext.Window({
    applyTo : "dialogPleaseWait", closable : false, modal : true,
    width : 200, height : 100, minimizable : false, resizable : false,
    draggable : false, shadowOffset : 8, id : "dialogPleaseWait"
  }).show(Ext.getDom("divSource"));
  setTimeout("organizerExt.initMain()", 500);

}

First, an Ext JS Window is opened. The applyTo attribute is set to dialogPleaseWait; therefore, the markup in index.htm that contains the content of the <div> dialogPleaseWait will be used to form the Window. Recall that the special "marker" styles were used in that markup, and now we can see why: the Window class knows about those markers and so can determine what content in the specified DOM node is the header for the Window, what is the main content, and so forth. We specifically make the window static in the sense that it can't be minimized (minimizable:false), can't be resized (resizable:false), and can't be dragged around (draggable:false). We also make it modal (modal:true), which makes it a typical lightbox pop-up (everything else on the page is dulled out and the Window is front and center with the full focus of the user on it). In other words, it's pretty well there until we tell it to go away. The code here is interesting in that the Window object is created, and then we immediately show it via the chained call to its show() method (which is passed a reference to the divSource <div> so that the animation of the Window flying in starts from that location, which as you'll recall is the upper-left corner of the page). This chaining of method calls is pretty common in Ext JS programming, and in JavaScript in general. If you've used the popular jQuery library, you'll know that this can be taken to an extreme, but some people find it to be a much better style; it's up to your own tastes in the end.

Finally, we see that a timeout is started with an interval of 500 milliseconds (half a second). This is done to ensure that before the rest of the initialization procedure happens, the Window has completed its animation. This is important because JavaScript is always single-threaded, so if we continued with the rest of our code the Window very likely would not be visible, and almost certainly wouldn't properly complete its animation (at best it would probably happen in a choppy, visually displeasing fashion).

Once that timeout occurs, it fires the initMain() method of the OrganizerExt class, which is up for examination next:

this.initMain = function() {

  if (!testForGears()) { return; }

  createDataStores();
  createRecordDescriptors();
  populateStores();
  createNewNoteDialog();
  createNewTaskDialog();
  createNewContactDialog();
  createNewAppointmentDialog();

Ext.QuickTips.init();
  Ext.form.Field.prototype.msgTarget = "side";

  buildUI();

  Ext.getCmp("dialogPleaseWait").destroy();

}

First, a call to the testForGears() method is made, so let's jump ahead slightly and look at that now:

var testForGears = function() {

  if (!dao.init()) {
    Ext.getCmp("dialogPleaseWait").destroy();
    var dialogNoGears = new Ext.Window({
      applyTo : "dialogNoGears", closable : false, modal : true,
      width : 400, height : 220, minimizable : false, resizable : false,
      draggable : false, shadowOffset : 8, closeAction : "hide",
      buttons : [{
        text : "Ok",
        handler : function() {
          dialogNoGears.hide();
        }
      }]
   });
   dialogNoGears.show(Ext.getDom("divSource"));
   return false;
 } else {
   return true;
 }

}

A call to the DAO class's init() method is made, which you'll recall returns true if Gears is good to go, and false otherwise. So, if we get false here we begin by destroying the dialogPleaseWait Window. Note that no animation occurs in this case—it's simply destroyed, which includes removing it from the screen straight away. After that, a new Window is created, this one using the contents of the dialogNoGears <div> as its template. For this Window there will be an OK button for the user to click to dismiss the Window. To do this we use the buttons configuration attribute to the Window constructor. This is a simple array of objects, where each object defines a button. We only have one button here, and when it's clicked we want to hide the Window (we could destroy it as well, but there's little different in this instance so I thought it would be nice to see something different than we saw with the Please Wait Window). One of the possible attributes on the object defining the button is the handler attribute, which is a reference to a function to execute when the button is clicked. In this case it's an inline function, since it's not needed anywhere else.

NOTE

We have a closure here: the dialogNoGears variable is a reference to the Window created, and it's still available to the callback function via closure. This makes for some clean, tight code, which is nice.

Finally, a call is made to the show() method of the new Window object. In this case I decided not to chain the method call as we saw previously, just to show a different syntax to you (see, I care about you, dear reader!)

A WORD ON CLOSURES

Although the assumption throughout this book is that the reader has a fair understanding of JavaScript, closures are one of those concepts that confuse the heck out of most developers until it finally just suddenly clicks. At that point, they see how very useful they are. Closures are not something that every developer has experience with, so I'll provide a brief description here.

A closure is an expression (typically a function) that can have free variables together with an environment that binds those variables (in other words, that "closes" the expression). Perhaps a simpler explanation is that functions in JavaScript can have inner functions. These inner functions are allowed to access all the local variables of the containing function, and most importantly, even after the containing function returns.

Each time a function is executed in JavaScript, an execution context is created. This is conceptually the environment as it was when the function was entered. That's why the inner function still has access to the outer function's variables even after return: the execution context of the inner function includes the execution context of the outer function.


Of course, if Gears was available, then the else branch would have hit, returning true, which gets us back into the main code of the initMain() method. In that case, a bunch of method calls execute. First is createDataStores(), which literally creates the four data stores, one for each category of items. Note that populating the stores with what may be in the Gears database is done later. Before that can occur, we need to create Record descriptors for notes, tasks, contacts, and appointments, and that's the result of calling the createRecordDescriptors() method.

Once those three methods complete, we have fully built data stores (although remember that they do not yet have data in them). Let's now we move on to creating the four dialog Window objects for creating notes, tasks, contacts, and appointments. There is a method call corresponding to each of them: createNewNoteDialog(), createNewTaskDialog(), createNewContactDialog(), and createNewAppointmentDialog(). We'll look at those shortly.

NOTE

I suppose, looking back on it now, creation of the data stores and Record descriptors could have been broken out into four separate methods like the creation of the Window dialogs are... I don't have any secret reason for doing it this way, but putting all the Window creation code together would have made for a much longer method!

Here are the next two lines of code:

Ext.QuickTips.init();
Ext.form.Field.prototype.msgTarget = "side";

This code configures Ext JS so that tooltips will work when validation failures occur on those four new item creation forms. It also indicates that, by default, the messages will be anchored to the side of the form elements. You can set this on a per-field or per-form basis, but doing so globally is better if you can, and in this case we can.

The final two tasks are to build the UI via a call to buildUI(), and to destroy the Please Wait Window. When that's done, the application is fully initialized and ready for user interaction.

The buildUI() is where most of the action is, but before we get to that we have a number of other methods to look at, starting with createDataStores().

3.5.2.3. The Data Stores

Creating the data stores isn't a big deal at all—in fact, it's another of those "if you've seen one, you've seen 'em all" situations. So, with that in mind, let's look at one:

organizerExt.notesStore = new Ext.data.Store({
  listeners : {
    "add" : {
      fn : function(inStore, inRecords, inIndex) {
        if (Ext.getCmp("dialogPleaseWait")) { return; }
        dao.createNote({
          id : new Date().getTime(),
          category : inRecords[0].get("category"),
          content : inRecords[0].get("content")
        });
      }
   },
   "remove" : {
     fn : function(inStore, inRecord, inIndex) {
       dao.deleteNote( { id : inRecord.get("id") } );
     }
   }
  }
});

A new Ext.data.Store() object is instantiated, and that might be the end of it except that we also need to add to it the ability to react to various events, namely adding Record objects to it and removing Record objects from it. This is done by including the listeners attribute in the configuration object passed into the constructor. The listeners attribute is an array of events, and objects contain information defining what happens in response to the event. So, the add event has an object that within it has a single attribute, fn. This is a reference to a function to execute when the add event fires. The signature of this callback method is defined in the Ext JS documentation for the add event. In this case it gets passed a reference to the data store itself, an array of Record objects being added (one or more Record objects can be added at a time), and the index at which the Record object will be added. For our purposes, we only care about that array of Record objects in actuality.

In the callback function itself, we do a quick check to see if the Please Wait Window is present. This is because any time a Record is added to the store, this function will execute. So, when the store is being populated initially, this will execute for each Record we add. Since at that point we know we don't want to save anything to the underlying database, we need to skip execution, and checking to see if that Window exists is an easy way to determine that. So, if the Window doesn't exist, all it takes is a call to the DAO class's createNote() method, passing it an object that contains all the data for a note, taken from the incoming inRecords array. In our use case, we know there's only one record and there's no iteration over the array to do, so we just go after the first, and only, element, directly.

When a Record is removed from the store, the same sort of thing occurs, but the remove event fires this time. In this case, we still pass an object to the DAO class's deleteNote() method, but this time it's only the id of the note to be deleted that we care about.

In order to make this clear, take a look at the sequence diagram[] in Figure 3-13. This walks you through the flow for adding, deleting, and even updating Record objects in both the data stores and the underlying Gears database. Hopefully this figure helps you see how it all ties together because an event-driven model like this can sometimes be difficult to wrap your brain around.

[] I personally dislike sequence diagrams. I find that usually the degree to which they are useful is inversely proportional to the amount of time the creator spent on it, and they are virtually never as useful as you expect them to be. Hopefully this is one of the exceptions to disprove the rule!

Figure 3.13. Sequence diagram depicting the creation of a note

Although I said that seeing one data store created is pretty much seeing how they're all created, I wanted to call out the tasksStore data store separately so you can see the update event handling in action:

organizerExt.tasksStore = new Ext.data.Store({
  listeners : {
    "add" : {
      fn : function(inStore, inRecords, inIndex) {
        if (Ext.getCmp("dialogPleaseWait")) { return; }
        dao.createTask({
          id : new Date().getTime(),
          status : inRecords[0].get("status"),
          category : inRecords[0].get("category"),
          content : inRecords[0].get("content")
        });
      }
    },
    "update" : {
      fn : function(inStore, inRecord, inOperation) {
        dao.updateTask({
          id : inRecord.get("id"), category : inRecord.get("category"),
          status : inRecord.get("status"), content : inRecord.get("content")
        });
        organizerExt.showTaskDetails(inRecord);
      }
    },
    "remove" : {
      fn : function(inStore, inRecord, inIndex) {
        dao.deleteTask( { id : inRecord.get("id") } );
      }
    }
  }
});

As you can see, the add and remove events are handled just as you saw a little while ago, but now we have the update event handled as well. It's not any different from the other event handlers, but I thought you'd like to see that for yourself! Note the use of new Date().getTime(), which returns a numeric value in milliseconds. This gives us a simple way to generate a unique identifier for a record that should be safe too. (If you change your PC's clock to a past date, or if you run the code so fast that multiple records are created in the same millisecond of time, a conflict could arise. But that seems unlikely in a JavaScript environment and especially so within the context of how this application works.)

NOTE

There are many events you can handle with regard to data stores, but here we only need these, and my suspicion is that most of the time these three events will be all you need. I'll name just a few others that might be of interest: clear (fires when the data cache is cleared), loadexception (occurs if an exception occurs in the proxy during loading) and datachanged (fires when the data cache has changed and a widget bound to the store should refresh its view). Consult the Ext JS documentation for the full list of events and the signatures for the callback functions to match.

3.5.2.4. The Record Descriptors

The record descriptors are instances of Ext.data.Record, and as such are a little more than simple value objects (VOs).[] However, by and large, that's exactly how you can think of them. Figure 3-14 is a diagram showing the structure of each of the four types of Record classes we'll be creating.

[] A VO is a construct that's seen most often in, but that's not limited to, the Java languages. It's simply a class designed for transferring data back and forth between two entities. This comes up in Java because you don't have structs like in C—classes are all there is. VOs usually contain no logic but just data fields, as well as accessor and mutator methods (a.k.a. getters and setters) for setting and accessing those fields.

Figure 3.14. The record descriptors in all their glory

The way it works is that you instantiate an Ext.data.Record object by calling the static create() method of that class, feeding an array of field descriptors to the constructor, like so:

organizerExt.NoteRecord = Ext.data.Record.create([
  { name : "id", mapping : "id" },
  { name : "category", mapping : "category" },
  { name : "content", mapping : "content" },
  { name : "type", mapping : "type" },
]);

The result of this is the creation of a new class, NoteRecord, which we make a public member of the OrganizerExt instance. As you can see, we're specifying that this type of Record has four fields: id, category, content, and type. Each field is defined by an object in an array, and the object has two attributes: name, which is simply the name of the field in the Record, and mapping, which is the name of the attribute of the underlying data object. So for example, when we create an instance of a NoteRecord, we use this code:

var nr = new organizerExt.NoteRecord({
  id : 123, category : "myCategory", type : "Note", content : "myContent"
});

With the name and mapping attributes defined as such, the Record knows that the id attribute of the incoming object maps to the id field of the Record, and so forth. Note that the mapping attribute is optional if it's the same as the name attribute, or so says the Ext JS documentation. However, I had trouble with the code working if I left it out, so I included it. Also note that the value of the mapping attribute depends on what underlying Ext.data.Reader implementation is creating the Record. For example, if you were using the Ext.data.JsonReader, it's a JavaScript expression to reference the data, whereas for the Ext.data.XmlReader, it's an Ext.DomQuery path to the element to map to the Record field. An example would be E[foo=bar], which matches an attribute foo that equals bar (see the Ext JS documentation for full details).

The other three Record types are pretty redundant, aside from differing in the fields they contain, so take a look on your own and let's move on to some other things.

3.5.2.5. Populating the Data Stores

Populating the data stores from the Gears database, done once at startup, is a pretty trivial task, as you can see for yourself:

var retrievedNotes = dao.retrieveNotes();
for (var i = 0; i < retrievedNotes.length; i++) {
  organizerExt.notesStore.add(
    new organizerExt.NoteRecord({
      id : retrievedNotes[i].id,
      category : retrievedNotes[i].category, type : "Note",
      content : retrievedNotes[i].content
    })
  );
}

A call to one of the retrieval methods in the DAO, retrieveNotes() in this case, gets us all the data there is to get. Remember that we get back an array of simple objects here, so the next step is to iterate over that array. For each item we create the appropriate Record, a NoteRecord here, and pass that Record to the add() method of the corresponding data store. This is all done as one statement, just because it felt natural to me to do it this way. (The alternative would have been to create a NoteRecord, assign it to a variable, and pass it along to the add() method, but it's purely a style choice.) The other four data stores are similarly populated, so again we'll save some space and move on to some other things.

3.5.2.6. The "New Note" (and, Indirectly, the "New Task") Dialog

The next method we run into as we walk through this class is the createNewNoteDialog() method, which does precisely what its name implies it does:

var createNewNoteDialog = function() {

  var createNoteFormPane = new Ext.FormPanel({
    id : "createNoteFormPane", monitorValid : true,
    frame : true, labelWidth : 70, width : 400, autoheight : true,
    items : [
      {
        xtype : "combo", fieldLabel : "Category", name : "category",
        width : 280, allowBlank : false, editable : false,
        triggerAction : "all",
        mode : "local", store : organizerExt.categoryList, typeAhead : false
      },
      {
        xtype : "textarea", fieldLabel : "Content",
        name : "content", width : 280, height : 230,
        allowBlank : false
      }
   ]
 });

 createNoteFormPane.addButton( { text : "Create", formBind : true},
   function() {
     var vals = Ext.getCmp("createNoteFormPane").getForm().getValues();
     var newNoteRecord = new organizerExt.NoteRecord({
       category : vals.category, content : vals.content, type : "Note",
       id : 0
     });
     organizerExt.notesStore.add(newNoteRecord);
     Ext.getCmp("dialogCreateNote").hide();
   }
 );
 createNoteFormPane.addButton("Cancel", function() {
   Ext.getCmp("dialogCreateNote").hide();
 });

 new Ext.Window({
   title : "Create Note", closable : true, modal : true,
   width : 400, height : 340, minimizable : false, resizable : false,
   draggable : true, shadowOffset : 8, items : [ createNoteFormPane ],
   closeAction : "hide", id : "dialogCreateNote"
 });

}

In the case of previous Windows, we created the Window and immediately showed it, but in this example we're creating the Window for later. This is a good technique when you know the Window (or other UI widget) is something you'll need over and over again. It's better to avoid the overhead of creation if you can by only creating it once and reusing it. That's precisely what we're doing here. We create a new Window, which starts off not visible.

Before we create the Window itself, though, we're creating a FormPanel. This is a widget that houses a form. On its own, a FormPanel, a descendant of the Panel class, doesn't do much. It has to be a child of some other widget to do much good. Here it's going to be a child of our Window.

Creating a FormPanel, and by extension a form, is not too tough. We start by instantiating an Ext.FormPanel object, passing into its constructor a configuration object. This object contains a number of fields, starting with id, which is pretty self-describing. Next is monitorValid, which is a neat option that tells the form to monitor itself to determine if it's "valid," whatever valid means in this context. This causes a looping event to occur whenever the valid state of the form changes. We can react to this state if we wish. More importantly, though, is that we can have other form elements tied to this state for free! Look down a bit in the code to where the Create button is created and note the formBind:true configuration option. This instructs the button to take part in that looping event so that whenever the form is not valid, the button is disabled, and when the form is valid, the button is enabled. This is precisely what you'd want to happen from a user interface perspective, and we got it without writing a lick of code. Very sweet!

Returning to the configuration of the FormPanel, we see the width and height specified, sized to fit nicely in our Window. We also see the frame:true attribute, which puts a nice frame border around the FormPanel, which just makes it look a little better within the Window. We also inform the FormPanel to set its height automatically (autoHeight:true) based on its contents. We also include a setting that specifies how wide the labels of our fields should be (labelWidth:70).

Following that is an array assigned to the items attribute. This will be the fields on our form. Each element of the array is an object that describes a given field. We start with the Category field. The xtype attribute tells us what kind of field this is going to be, a combo box in this case. We specify a label for the field via the fieldLabel attribute, and we assign a name to the field to retrieve its value later. The width attribute tells us how wide the field itself will be, minus the width of the label. (You'll notice the field is 280 pixels wide, and the label is 70 pixels wide, leaving 50 pixels in the width of the FormPanel and Window, which is enough to display the error icon when a field is invalid.) The allowBlank attribute, when set to false, indicates that this field is required in order for the form to be valid. This is how the mechanism enabled by monitorValid, and by extension the formBind attribute of the Create button, knows whether or not a given field is valid at a given point in time.

Setting the editable attribute to false indicates that the user should not be able to type a value into the combo box, which makes it work more like a traditional <select>.

NOTE

I encountered a problem while writing this code where once you select a value, the list of values no longer appeared. A user in the support forums on the Ext JS web site came to the rescue, indicating that the solution was to set triggerAction to all, as you see here. This essentially tells the combo box to re-query the data that is used to populate it any time the field is triggered—in this case, when the down arrow is clicked.

The mode attribute indicates whether the data for the combo box is coming from a remote source—a server in other words—or from local JavaScript. Here we have no server and a set list of options, so it's local. We also need to tell the ComboBox where the store of data is by specifying the store attribute. Here, it's simply pointing at the simple array referenced by the categoryList attribute of the OrganizerExt class.

A TextArea is next created for users to type their note into, which is where the xtype of textarea comes into play. Its attributes are obvious, given that we've seen them all already.

The FormPanel now has all the fields the user can enter, so all that remains is adding some buttons to it, one for actually creating the note and one for canceling, if the user changes his or her mind (after all, creating a note is a lifelong commitment, no?). The addButton() method of the FormPanel is made especially for this purpose. First the Create button is created. The first argument to this method is a configuration object, here specifying the text on the button and that formBind attribute discussed earlier.

The second argument of the addButton() method is a function to execute when the button is clicked. Here, this function doesn't have to do a whole lot: it gets a reference to the FormPanel via the Ext.getCmp() function, then gets the form contained within that FormPanel. It then calls the getValues() method of that form, which returns a simple object where the attributes correspond to the form fields, with their corresponding values assigned to the attributes. This object, reference by the vals variable, is used to create a NoteRecord object, which is then passed to the add() method of the notes data store. As we saw previously, this triggers the add event, which calls our DAO.createNote() method, which in turn writes the data to the Gears database. Did you notice how relatively little code we had to write to get the connection between the database, DAO, data store, and form? It probably amounts to 20 lines of code or so all totaled, which isn't much at all!

The cancel button is created similarly, and its function simply calls the hide() method on the Window. Remember, we're going to reuse this Window; hence we don't want to destroy it but just hide it.

The final step in this method is to create the actual Window. You've seen that a few times now, so it's nothing new. This time around, though, we specify the closeAction attribute, which is what will happen if the user clicks the close X icon on the Window. Again, we just want to hide it. One difference in the way this Window is created is that previously we saw how to create a Window based on some existing HTML, but this time around we're building it completely via code. So, the items attribute is used to attach various UI widgets to the Window, in this case our FormPanel, which contains all the content. Likewise, we have to explicitly specify a title for the Window.

In Figure 3-15 you can see the results of all this effort.

Figure 3.15. The New Note window

In Figure 3-16 you can see what happens when a form is not valid. The tooltip you see is a result of me hovering the mouse over the red exclamation bubble next to the field. Also notice that the Create button has been disabled. Again, note that we accomplished that with just some configuration options—we didn't have to write any code to do it.

If you play with the contacts creation Window, you'll notice that some other types of validations are present. For example, the e-mail address value must be a valid e-mail address. This too is something we can get automatically, like so:

{
  xtype : "textfield", fieldLabel : "eMail Address",
  name : "email", width : 250, vtype : "email"
}

This is the object in the items array that is specified for the FormPanel in the create contact Window. That vtype attribute, short for validation type, specifies a type of validation to occur for that field. Table 3-1 lists the other vtypes built into Ext JS.

Figure 3.16. The New Task window, with a validation failure and the associated tooltip

Table 3.1. Vtypes Available by Default in Ext JS
VtypeDescription
emailEnsures the field's value is a valid e-mail format (i.e., [email protected])
urlEnsures the field's value is a valid URL format (i.e., http://www.domain.com)
alphaEnsures the field's value only contains letters and the underscore character
alphanumEnsures the field's value only contains letters, numbers, and the underscore character

It is also possible to create your own validation types, or extend these to meet your needs. You'll see an example of that in later chapters.

3.5.2.7. The New Appointment Dialog

Even though there's not much more to it than you've seen already, let's take a quick look at the New Appointment Window:

var createNewAppointmentDialog = function() {

  var createAppointmentFormPane = new Ext.FormPanel({
    id : "createAppointmentFormPane",
    frame : true, labelWidth : 100, width : 400, autoheight : true,
    monitorValid : true,
    items : [
      {
        xtype : "textfield", fieldLabel : "Title",
        name : "title", width : 250, allowBlank : false
      },
      {
        xtype : "combo", fieldLabel : "Category", name : "category",
        width : 250, allowBlank : false, editable : false,
        triggerAction : "all",
        mode : "local", store : organizerExt.categoryList
      },
      {
        xtype : "datefield", fieldLabel : "When",
        name : "whendt", width : 250, allowBlank : false
      },
      {
        xtype : "textfield", fieldLabel : "Location",
        name : "location", width : 250
      },
      {
        xtype : "textarea", fieldLabel : "Note",
        name : "note", width : 250, height : 152
      }
    ]
  });

  createAppointmentFormPane.addButton( { text : "Create", formBind : true},
    function() {
      var vals = Ext.getCmp("createAppointmentFormPane").getForm().getValues();
      vals.whendt = Date.parseDate(vals.whendt, "m/d/Y");
      var newAppointmentRecord = new organizerExt.AppointmentRecord({
        title : vals.title, category : vals.category, whendt : vals.whendt,
        location : vals.location, note : vals.note, type : "Appointment",
        id : 0
      });
      organizerExt.appointmentsStore.add(newAppointmentRecord);
      Ext.getCmp("dialogCreateAppointment").hide();
    }
  );

createAppointmentFormPane.addButton("Cancel", function() {
      Ext.getCmp("dialogCreateAppointment").hide();
    });

      new Ext.Window({
        title : "Create Appointment", closable : true, modal : true,
        width : 400, height : 340, minimizable : false, resizable : false,
        draggable : true, shadowOffset : 8, items : [ createAppointmentFormPane ],
        closeAction : "hide", id : "dialogCreateAppointment"
    });

  }

The only thing really different here is the introduction of two new xtype values: datefield and textfield. The former creates a field with a calendar icon that, when clicked, presents a full calendar from which to choose a date. The textfield xtype is just like textarea except it is a single-line place for the user to type a value.

You can also here see the use of the Date.parseDate() method when getting the value of the When field. Because I wanted the data type of the whendt field in the AppointmentRecord to be a true JavaScript Date object, and since using getValues() on a form like this only gets you strings, I needed to parse that string into a Date object. That is exactly what parseDate() does for us. The second argument to this function indicates what the format of the input date string is, and gives us back a true Date object. Very nice!

NOTE

The call to getForm() on the FormPanel gives us back an Ext.form.BasicForm, which has a number of neat methods, getValues() among them. This is very much like an HTML form with all sorts of handy utility methods hanging off it, and I suggest you spend a few minutes with the Ext JS API documentation on the BasicForm class to become familiar with what it has to offer.

In Figure 3-17 you can see what the New Appointment Window looks like. I've even clicked the calendar icon next to the When field to show the calendar in all its glory.

Figure 3.17. The New Appointment dialog

3.5.2.8. Building the User Interface: The buildUI() Method

As it happens, building the UI pretty well boils down to one massive JavaScript statement! You can take one of three approaches to building a UI with Ext JS. The first is what we'll see in this application: one giant statement. The second is to create a whole bunch of components programmatically and then stitch them together via code. The third is more of a declarative approach where you do as much as possible in markup, which is then used as the templates for the widgets created via code (which we've seen some of here). The bottom line is you're going to be writing code one way or another—it's just a question of how much and in what form.

3.5.2.9. Creating the Viewport

The first step is typically creating a Viewport. There is exactly one Viewport per page that takes up the entire browser window, and inside the Viewport goes various other Container components (usually Panel objects). Let's see how that Viewport is created first. Note that this begins that giant statement I mentioned, and all the code snippets that follow are parts of that statement.

var vp = new Ext.Viewport({
  layout : "border",
  items : [{

The Ext.Viewport class is instantiated and passed a configuration object. The layout attribute specifies how the Viewport will organize its child components. A number of layouts are available, as you can see in Table 3-2. Note that these don't only apply to the Viewport; they can also in most cases be applied to children of the Viewport.

Table 3.2. Layouts Available to the Viewport and Child Panels
Layout Attribute ValueDescription
absoluteThis layout inherits the anchoring of Ext.layout.AnchorLayout and adds the ability for x/y positioning using the standard x and y component config options.
accordionThis layout contains multiple panels in an expandable accordion style so that only one panel can be open at any given time. Each panel has built-in support for expanding and collapsing.
anchorThis layout enables anchoring of contained elements relative to the container's dimensions. If the container is resized, all anchored items are automatically rerendered according to their anchor rules.
borderThis is a multipane, application-oriented UI layout style that supports multiple nested panels, automatic split bars between regions and built-in expanding and collapsing of regions.
cardThis layout contains multiple panels, each fit to the container, where only a single panel can be visible at any given time. This layout style is most commonly used for wizards, tab implementations, or other UI interfaces where multiple "pages" are present in the same area but only one is visible at a time.
columnThis is the layout style of choice for creating structural layouts in a multicolumn format where the width of each column can be specified as a percentage or fixed width but the height is allowed to vary based on the content.
fitThis layout contains a single item that automatically expands to fill the layout's container.
formThis layout is specifically designed for creating forms. Typically you'll use the FormPanel instead.
tableThis layout allows you to easily render content into an HTML table. The total number of columns can be specified, and rowspan and colspan can be used to create complex layouts within the table.

There is also a basic Container layout, which is what you'll get for the Viewport if you supply no value for the layout attribute. In this case we're using a Border layout, since the structure of the application fits that model nicely: there's something on the top (the toolbar), something on the left (our Accordion category selector), and something in the middle (both the icon/list views and the details section). Note that you do not have to use all of the areas allowed for with a Border layout, you can skip any you like.

3.5.2.10. Creating the Accordion Pane

Once the Viewport is created and configured, we can start adding children to it, starting with the Accordion on the left where the user can flip between categories and filter listings. The code that begins creation of the Accordion looks like this:

region : "west", id : "categoriesArea", title : "Categories",
split : true, width : 260, minSize : 10, maxSize : 260,
collapsible : true, layout : "accordion",
defaults: { bodyStyle : "overflow:auto;padding:10px;" },
layoutConfig : { animate : true },
items : [

The region attribute specifies in what position of the Border layout this child should live in—in this example, the left side, or west area. An id can be assigned to this Accordion if you want to, as I've done here. You can set the title attribute to have a title bar present. The split attribute, when true, indicates that there is a split bar that the user can drag to resize this area. The width attribute specifies the starting width of the Accordion, and the minSize and maxSize attribute specify the minimum and maximum width the Accordion can have when the user resizes it. The collapsible attribute set to true includes the little arrow in the title bar of the Accordion that allows the user to quickly collapse this section of the layout. The defaults attribute specifies attributes to be applied to any component added to this Container. Here it applies some padding to the body of the content in each of the Accordion panes. The layoutConfig attribute is an object whose properties set attributes specific to the component being created. So here, for example, we're specifying that the Accordion should animate itself whenever the user flips between panes.

Once you have the Accordion all set up, you can begin to add components to it, and in this case I've done so using the items attribute.

3.5.2.11. The Appointments Accordion Pane

Each element of the items attribute array is an object that defines a component to be added to the Accordion, creating another pane in the Accordion. The first one here is creating the pane for appointments:

{
  listeners : {
    "expand" : {
      fn : function() { organizerExt.changeCategory("appointments"); }
    }
  },
  id : "appointmentsArea", title : "Appointments",
  items : [
    { xtype : "label", text : "Filter:" },
    { xtype : "label", html : "<br><br>" },
    {
      xtype : "radiogroup", columns : 1,
      items : [

{ boxLabel : "Show All", name : "appointmentsFilterType",
      inputValue : 1, checked : true,
      listeners : {
        "check" : function(inCheckbox, inChecked) {
         if (inChecked) {
           organizerExt.appointmentsStore.filterBy(
             function(inRecord, inIndex) {
               return true;
             }
           );
          }
         }
        }
       },
      { boxLabel : "Show Date:",
        name : "appointmentsFilterType", inputValue : 2,
        listeners : {
          "check" : function(inCheckbox, inChecked) {
            var afcDatePicker =
              Ext.getCmp("appointmentsDatePicker");
            if (inChecked) {
              afcDatePicker.enable();
            } else {
              afcDatePicker.disable();
              afcDatePicker.setValue(new Date());
            }
           }
          }
         }
        ]
      },
      {
        xtype : "datepicker", id : "appointmentsDatePicker",
        disabled : true,
        listeners : {
          "select" : function(inPicker, inDate) {
            organizerExt.appointmentsStore.filterBy(
              function(inRecord, inIndex) {
                var whendt = inRecord.get("whendt");
                if (whendt.getMonth() == inDate.getMonth() &&
                  whendt.getDate() == inDate.getDate() &&
                  whendt.getFullYear() == inDate.getFullYear()) {
                  return true;
                } else {

return false;
          }
         }
       );
      }
     }
    }
  ],
  border : false, cls : "cssDefault"
},

The first thing we see is that an event handler is defined by passing an array as the value of the listeners attribute. Each element in the array is an object with attributes named after events, as we've previously seen. Here, we call the changeCategory() method of the OrganizerExt class any time this pane is expanded.

After that we give an id and title to this particular pane, and then it's time to add some content to the pane. For that we use the items array once again.

The first two items added are xtype:label, which is just a simple string of text. Note that you can have HTML as the value here, and that renders as you'd expect it to. Following that is the addition of an xtype:radiogroup. This is some number of Radio buttons grouped together so that they function as mutually exclusive selections, just like Radio buttons should.

The columns attribute specifies how many columns should be used to render the Radio buttons, and this allows you to have matrixes of Radio buttons rather than just running right down the page vertically, as is the case here. Within the RadioGroup we again see an items array (this is a common thread if you haven't noticed by now!) where each element is a Radio button. Each Radio button has a boxLabel attribute, which is a fancy way of saying it's the text accompanying the Radio button, and a name attribute.

NOTE

The name attribute must match all the other Radio buttons in the RadioGroup; otherwise, they don't function in the expected way (i.e., only one selection allowed). This was frankly a little surprising to me; I expected that making the Radio buttons part of a RadioGroup made the name not matter, but that isn't the case.

The inputValue attribute allows you to assign a value to each Radio button, and the checked attribute determines which of the options is selected upon creation. Finally, we see another use of the listeners attribute to define some code to execute when the Radio button is selected by responding to the check event. In the case of the first Radio button, we filter the list currently being shown (although since this Radio button is the Show All option, "filtering" here means selecting all items from the store). Note that the check event fires when the item is checked or when it is unchecked, hence the reason the inChecked argument is taken into account: we only want this code to execute when the Radio button has been selected, not when it's been unselected.

NOTE

You may at first find the attribute checked, and the event named check, to be strange for a Radio button, since they don't really get "checked." However, a quick look at the Ext JS API docs clears it up: Radios extend from the Checkbox widget, and it inherits these items, among others. Remember, Ext JS is built based on a rich OOP foundation, and that shines through frequently like this.

In the case of the second Radio button, we need to enable the DatePicker if the Radio button has been checked, or disable the DatePicker if the Radio button has been unchecked. The call to Ext.getCmp("appointmentsDatePicker"); gets us a reference to the DatePicker, which we can then call enable() or disable() on as appropriate. Also, when the DatePicker is disabled, its date is set to today's date so that it's ready for the next time it's needed.

The DatePicker itself is the next item added to this Accordion pane using an xtype of datepicker. It starts out disabled (disabled:true) since the Show All Radio button is by default selected.

When the select event fires on the DatePicker, we execute some code to again filter the items shown in the list. The filterBy method of the target data store is used to filter the items. Recall that this method accepts a reference to a callback function that will be called for every Record in the store. The function returns true if the Record should be included, False if not. Because the appointments DataView objects are bound to the data store, they are updated automatically.

3.5.2.12. The Notes Accordion Pane

The Accordion pane for notes is conceptually very similar to the one for appointments, and the same is true for contacts and tasks. So, while I'll show the code for the notes pane here, I'm going to skip over much of it on the basis that you've already seen most of it, and I'll only be pointing out some differences for tasks and contacts.

{
  listeners : {
    "expand" : {
      fn : function() { organizerExt.changeCategory("notes"); }
    }
  },
  id : "notesArea", title : "Notes", border : false,
  cls : "cssDefault",
  items : [
    { xtype : "label", text : "Filter:" },
    { xtype : "label", html : "<br><br>" },
    {
      xtype : "radiogroup", columns : 1,
      items : [

{ boxLabel : "Show All", name : "notesFilterType",
      inputValue : 1, checked : true,
      listeners : {
        "check" : function(inCheckbox, inChecked) {
          if (inChecked) {
            organizerExt.notesStore.filterBy(
              function(inRecord, inIndex) {
                return true;
              }
            );
           }
          }
         }
        },
       { boxLabel : "Show Category:",
         name : "notesFilterType", inputValue : 2,
         listeners : {
           "check" : function(inCheckbox, inChecked) {
            var nfcCombo = Ext.getCmp("notesFilterCategory");
            if (inChecked) {
              nfcCombo.enable();
            } else {
              nfcCombo.disable();
              nfcCombo.reset();
            }
          }
         }
        }
       ]
      },
      {
        xtype : "combo", id : "notesFilterCategory", editable : false,
        mode : "local", store : organizerExt.categoryList,
        disabled : true, triggerAction : "all",
        width : 150, listWidth : 168,
        listeners : {
          "select" : function(inComboBox, inRecord, inIndex) {
           organizerExt.notesStore.filterBy(
             function(inRecord, inIndex) {
               if (inRecord.get("category") ==
                 Ext.getCmp("notesFilterCategory").getValue()) {
                 return true;
               } else {

return false;
         }
        }
      );
     }
    }
   }
  ]
},

The primary difference between this and what we just looked at for appointments is that instead of a DatePicker we have a ComboBox for the user to choose a category to filter by. This uses an xtype of combo and is nearly the same as the ComboBox creations we saw earlier in the item creation dialogs. In fact, it is bound to the same basic array of data as those others were. Here, though, we have the addition of an event handler for the select event. The callback defined kicks off a call to filterBy() on the data store for notes, just as you'd expect. A simple comparison is all it takes to implement the required filtering logic.

In Figure 3-18 you can see what the ComboBox looks like in action.

Figure 3.18. The category being used to filter notes

As you can see, it's basically the same as in the creation dialogs, just as you'd expect.

3.5.2.13. Filtering Tasks by Status

Within the configuration for the tasks pane is the callback executed in response to The check event of the Show Active Radio button. I'd like to point that out here:

{ boxLabel : "Show Active", name : "tasksFilterType",
  inputValue : 2,
  listeners : {
    "check" : function(inCheckbox, inChecked) {
      if (inChecked) {
        organizerExt.tasksStore.filterBy(
          function(inRecord, inIndex) {
            if (inRecord.get("status") ==
              DAO.TASK_STATUS_ACTIVE) {
              return true;
            } else {
              return false;
            }
          }
        );
      }
    }
  }
},

This code uses one of the pseudo-constants from The DAO class in the comparison, but aside from that it's just like what we've seen already.

3.5.2.14. Filtering Contacts by Last Name

Like the active task filtering, contacts have a slightly different filtering capability, which is the ability to filter by last name:

{ boxLabel : "Show last nams starting with A-C",
   name : "contactsFilterType", inputValue : 2,
  listeners : {
    "check" : function(inCheckbox, inChecked) {
      if (inChecked) {
        organizerExt.contactsStore.filterBy(
          function(inRecord, inIndex) {
            var firstLetter = inRecord.get(
              "lastname").charAt(0).toUpperCase();
            if (firstLetter >= 'A' && firstLetter <= 'C') {
              return true;
            } else {

return false;
         }
       }
     );
    }
   }
  }
},

This is the definition for just one of a number of Radio buttons, each corresponding to a group of letters (A–C here, the next is D–F, and so forth). When one of these is selected, the check event fires, and for each Record in the data store we check if the first letter of the lastname field starts with one of the letters in the selected group. If so, true is returned; if not, false is returned, and that gives us filtering by last name.

3.5.2.15. The Main Region

The main, or center region of the BorderLayout, is where you find the icon/grid views and the detail section for a given category. You can generally split up a given Container as many times as you wish using various layouts, so here we have the center Container split into multiple regions by using a BorderLayout again within it:

{
  id : "mainArea", region : "center", layout : "border",
  items : [

This means we have a BorderLayout nester inside the center region of another BorderLayout. That also means that we could have up to five Containers (north, south, east, west, and center) as part of the inner BorderLayout. As it turns out, we'll only need two, though.

3.5.2.16. The Icon and Grid Lists

The north region of this inner BorderLayout is where we'll put a series of Panels, eight of them: four Panels for the icon view of each of the four categories, and four Panels for the list view of each of the four categories. To achieve this we'll use a CardLayout in this region:

{
  xtype : "panel", region : "north", split : true,
  collapsible : true, id : "listingCard", layout : "card",
  activeItem : 0, title : "Appointments",
  height : 175, autoScroll : true,
  items : [

Again, all it takes is setting The layout attribute to card and we have what we want. A CardLayout stacks a number of Panels on top of one another so that only one is visible at a time. The activeItem attribute indicates which of the Panels is active to begin with using a simple 0-based index value. The autoScroll attribute set to true indicates that content larger than the area occupied by the CardLayout will be allowed to scroll. The region attribute tells the component that contains this CardLayout (the inner BorderLayout) what region this CardLayout should be rendered to—the north region in this case.

Now that we have a CardLayout, let's see how an icon view is created. We'll just look at one since—you guessed it—they are all virtually identical!

3.5.2.17. An Icon View

An icon view is a DataView, defined by using an xtype of dataview:

{
  xtype : "dataview", id : "dvAppointmentsIconView",
  store : organizerExt.appointmentsStore,
  tpl : new Ext.XTemplate(
    "<tpl for=".">",
      "<div class="thumb-wrap">",
      "<div class="thumb">" +
      "<img src="img/iconView{type}.gif"></div>",
      "<span class="x-editable">{title}</span></div>",
    "</tpl>",
    "<div class="x-clear"></div>"
  ),
  singleSelect : true, overClass : "x-view-over",
  itemSelector : "div.thumb-wrap",
  listeners: {
    selectionchange : {
      fn : function(inDataView, inNodes) {
        var selectedRecord = inDataView.getSelectedRecords()[0];
        organizerExt.showAppointmentDetails(selectedRecord);
      }
    }
  }
},

A DataView allows us to display items however we choose by specifying a template to use to render each item. The tpl attribute specifies the Ext.XTemplate object to use for this purpose, and here we're creating a new one inline. As you can see, it consists of some basic HTML with the addition of some replacement tokens in it and some special processing tags. For example, the {title} token will be replaced with the title of a given appointment as entered by the user.

In addition to these tokens are some simple processing tags that can be used as part of the template. One such tag is <tpl for="."> (the quotation marks are escaped in the code, but this is the underlying statement). This special tag is equivalent to saying, "This template should be applied to all Records in the data store." More precisely, the template should never be applied to Records matched by the filterBy() method currently in effect, because the user could have chosen to filter the data based on some selected criteria from the Accordion panes.

After the template we find a few more configuration options. The singleSelect attribute, when true, indicates that only a single item in the DataView can be selected at a given time. The overClass attribute specifies the style class to apply to an item when the mouse is hovering over it. The itemSelector attribute indicates the style class to apply to the selected item. There is also the listeners attribute, as we've seen plenty of already. This time, we react to the selectionchange event, which results in the selected Record being retrieved via a call to inDataView.getSelectedRecords(). This method returns an array, but since we know only one item can be selected, it's the item at array index 0 that we're interested in. A call to organizerExt.showAppointmentDetails() is then made, passing along the Record object that was just retrieved. We'll see how those details are displayed soon, but first we need to look at a list view.

3.5.2.18. A List View

Creating a list view means creating a Grid, as you can see here:

{
  xtype : "grid", id : "gdAppointmentsListView",
  autoExpandColumn : "colTitle", minColumnWidth : 10,
  autoExpandMin : 10, autoExpandMax : 5000,
  store : organizerExt.appointmentsStore,
  columns : [
    {
      header : "Category", width : 50,
      sortable : true, dataIndex : "category"
    },
    {
      header : "Title", id : "colTitle",
      sortable : true, dataIndex : "title"
    },
    {
      header : "When", width : 90,
      sortable : true, dataIndex : "whendt"
    }
  ],
  viewConfig : { forceFit : true }, stripeRows : true,
  sm : new Ext.grid.RowSelectionModel({ singleSelect : true }),
  listeners: {
    rowclick : {
      fn : function(inGrid, inRowIndex, inEventObject) {
        organizerExt.showAppointmentDetails(
          inGrid.getSelectionModel().getSelected()
        );
      }
    }
  }
},

The grid xtype does the basic work for us, and then it's a matter of setting configuration options and defining the columns. The options are autoExpandColumn, which names the column that will expand to take up all the space in the grid; minColumnWidth, which is the smallest the user can make a column by dragging to resize it; autoExpandMin, which is the minimum space the column named by autoExpandColumn can take; and autoExpandMax, which is the maximum width of the column named by autoExpandColumn. The store attribute is the same as we've seen earlier: it binds the Grid to a particular data store, the one for appointments in this case.

After that is The columns attribute, which is an array of objects where each object defines a column in the Grid. Each object has a header attribute, which is the text to show in the column header; a width attribute, which is the initial width of the column; a sortable attribute, which indicates whether the user can click the column header to sort the data (true) or not (false); and dataIndex, which is the name of the field in a Record taken from the data store that we want displayed in that column.

Following The columns attribute are a few more configuration options (I probably should have grouped them all together, but Ext JS doesn't care, so it's purely a question of style). The first is viewConfig, which contains options that will be applied to the Grid's UI. The lone attribute within that object, forceFit, when true indicates that we want the Grid columns automatically expanded to fit the Grid in order to avoid horizontal scrolling. Next we see the stripeRows attribute set to true, and that does some color striping of the row to make it easier to read across its rows.

After that is The sm attribute, whose value should be (and is in this case) an instance of Ext.grid.RowSelectionModel. This defines how the user is allowed to select rows. Here, users can select only one at a time, so the singleSelect attribute of the config object passed to the constructor is set to true.

Finally, we have our friendly listeners attribute, and this time it's the rowclick event we're interested in. When a row is clicked, we need to show the details of the selected appointment via a call to organizerExt.showAppointmentDetails(). Passed to the callback for the event is inGrid, a reference to the Grid object; inRowIndex, literally the index number of the row that was clicked; and inEventObject, an object describing the event. The one we're interested in here is inGrid, because we can call the getSelectionModel() method to return the SelectionModel object we attached to the Grid, which contains a method for getting the Record associated with the clicked row via a call to getSelected(). That's all that is involved, aside from showing the details, which is coming up just a little later. For now, let's see how the areas where the details are shown are created, since that's obviously necessary before we can show details!

3.5.2.19. Item Details Panes

The details sections create another CardLayout, just like the Panel where the icon and list views are housed, one card for each category for which we might want to show details. Each of the four detail sections is defined by an object in the items array:

{
  xtype : "panel", region : "center", id : "detailsCard",
  layout : "card", activeItem : 0, autoScroll : true,
  title : "Appointment Details",
  items : [

{
    autoScroll : true, xtype : "panel",
    html : Ext.getDom("divAppointmentDetails").innerHTML
  },
  {
    autoScroll : true, xtype : "panel",
    html : Ext.getDom("divNoteDetails").innerHTML
  },
  {
    autoScroll : true, xtype : "panel",
    html : Ext.getDom("divTaskDetails").innerHTML
  },
  {
    autoScroll : true, xtype : "panel",
    html : Ext.getDom("divContactDetails").innerHTML
  }
 ]
}
]
},

Each of the detail sections is defined in index.htm as we saw earlier, and here we're setting the HTML that is to be displayed in a Panel. You see, it's not necessary to create Ext JS widgets and add them to a Panel; you can insert plain old HTML if you want via the html attribute. Since we already have the HTML we want to insert sitting on index.htm, all we need to do is get a reference to the appropriate <div> with a call to Ext.getDom() and get the innerHTML property. That becomes the value of the html attribute for the Panel, and voilà, we have content in the Panel!

3.5.2.20. Defining the Toolbar

Only one thing remains for us to define in our Viewport: the toolbar area up at the top. This is the closing section of that giant statement defining the Viewport that we started with, so let's dive right into it:

{
  id : "toolbarArea", autoHeight : true, border : false,
  region : "north",
  items : [{
    xtype : "toolbar", items : [
      {
        text : "New Note",
        handler : function() {
          Ext.getCmp("createNoteFormPane").getForm().reset();
          Ext.getCmp("dialogCreateNote").show(Ext.getDom("divSource"));
        }, icon : "img/toolbarNote.gif", cls : "x-btn-text-icon"
      },

{ xtype : "tbspacer" }, { xtype : "tbspacer" },

Thus begins The Toolbar definition. It lives in the north region of the main BorderLayout and has no border (it looks a little weird with the border present). The first item on the toolbar is the New Note button. When you click that button, the handler function is executed. Its job is to clear the form where the user enters note information. This requires a call to Ext.getCmp("createNoteFormPane") to get a reference to the FormPanel, followed by a call to getForm() to get a reference to the underlying form. Finally, reset() is called to do the actual dirty work of resetting the form. Once that's done, we can go ahead and show the Create Note Window.

{
  text : "About OrganizerExt",
  handler : function() {
    var dialogAbout = new Ext.Window({
      applyTo : "dialogAbout", closable : true, modal : true,
      width : 400, height : 320, minimizable : false,
      resizable : false, draggable : false, shadowOffset : 8,
      closeAction : "hide", buttons : [{
        text : "Ok",
        handler : function() {
          dialogAbout.hide();
        }
      }]
    });
    dialogAbout.show(Ext.getDom("divSource"));
  }, icon : "img/toolbarAbout.gif", cls : "x-btn-text-icon"
},
{ xtype : "tbspacer" },
{ xtype : "tbseparator" },
{ xtype : "tbspacer" },

We're putting the toolbar at the top of our BorderLayout, so we set the region attribute to north. The autoHeight attribute allows this Panel to set its height according to its contents, which is necessary for the toolbar to appear at all. We also indicate we don't want a border via border:false, since that would just look a bit wrong.

Next, The items array contains an object with an xtype of toolbar, and that's all it takes essentially to create a toolbar. The items array in that object then contains an object for each button to add, as well as spacers. For example, we see here how the New Note button is created. The text attribute is what you see on the screen. The handler attribute is the code to execute what the button is clicked. Here that's simply to get a reference to the form contained within the createNoteFormPane that we created earlier, resetting it via a call to its reset() method, and then showing the dialogCreateNote. Remember that this dialog Window was created but initially not shown, so we can use the Ext.getCmp(), which returns a reference to an existing Ext JS widget (Component, technically). We then just call show() on it, giving it a reference to our animation source <div>.

The icon attribute points to the image file to use as the icon for our button. This is optional, but we do want one because it spices up the toolbar a bit. Finally, the cls attribute is the style class to apply to the button, which here is supplied by Ext JS itself.

You'll also notice a number of objects with an xtype of tbspacer. These are just blank spaces you can use to spread things out a bit. Likewise, the tbseparator xtype puts a vertical line on the toolbar to break things up into logical groups, as you can see in Figure 3-19.

Figure 3.19. An example of the tbseparator xtype

The rest of the buttons are pretty similar to what we just saw. The button for switching between icon view and list view has a little more meat to it, so let's take a look at it:

{
    text : "Icon View", id : "tbViewMode",
    handler : function() {
      this.setText(this.getText().toggle("Icon View", "List View"));
      var iconImage = "url(img/toolbarIconView.gif)";
      if (this.getText() == "List View") {
        iconImage = "url(img/toolbarListView.gif)";
      }
      this.getEl().child("button:first").dom.style.backgroundImage =
        iconImage;
      organizerExt.changeViewMode(this.getText());
     }, icon : "img/toolbarIconView.gif", cls : "x-btn-text-icon"
    }
   ]
  }]
}

While the button itself is defined in the same way, the click event handler has a bit more going on. First, we toggle the text shown on the button between icon view and list view. To do this we use the handy toggle() method that Ext JS added to the String class. This avoids an if statement or a trinary logic statement. Next we need to toggle the icon on the button as well. To do this we call this.getText(), which gives us the text currently on the button (which remember, we just changed!). Using this, we set the appropriate value for the image file to use. We do this by altering the background-image style attribute (backgroundImage in JavaScript) of the first child of the button. You see, the icon is placed on the button by setting it as the background-image of the <div> the button is contained within. By using this.getEl(), which returns the underlying Ext.Element object that represents this widget in the DOM, we can then call the child("button:first") method to get a reference to the appropriate DOM node. Then we simply set its backgroundImage style attribute to point to the new image. Finally, a call to organizerExt.changeViewMode(), a method we'll see soon, does the actual switch between icon view and list view in the rest of the UI.

NOTE

This switching of the icon involved trial and error and looking at Ext JS example code. I couldn't find any documentation that explicitly spelled this out, but it's a testament to the quality of the Ext JS API documentation, along with the example code and support forums, that I was able to figure it out without too much trouble.

3.5.2.21. Adding Buttons to the Detail Panes

Now that the UI has been largely constructed, there are a few loose ends to tie up to complete the UI. Recall that when we created the sections where item details are displayed, we left placeholders for the buttons, but we didn't create the buttons. Well, now it's time to turn around and do just that! All the detail panes have a Delete button, so let's look at just one as a representative example:

new Ext.Button({
  text : "Delete Appointment", renderTo: "tdAppointmentDetailsDeleteButton",
  handler : function() {
    var viewMode = "IconView";
    if (Ext.getCmp("tbViewMode").getText() == "List View") {
      viewMode = "ListView";
    }

    organizerExt.appointmentsStore.remove(
      Ext.getCmp("dvAppointments" + viewMode).getSelectedRecords()[0]
    );
  }, disabled : true, id : "btnAppointmentDeleteButton"
});

We instantiate The Ext.Button() class, passing it a configuration object during construction. This object has a couple of attributes. First, text is literally the text to display on the button. Second, renderTo gives the ID of a DOM node to put the button into; if you look back, you'll see that tdAppointmentDetailsDeleteButton is the ID of the placeholder <td> we created (remember that the Window was created from existing markup, which contained a table structure). The handler attribute gives a reference to a function, inline in this case, to execute when the button is clicked. The first thing we need to do in this handler is to determine whether the user currently sees the icon view or the list view. We do this by interrogating the text of the view switch toolbar button, just as we saw previously. This is necessary because we can then construct the proper ID of the DataView to reference, which we must do in order to ask it for a reference to the selected Record from the data store. With that Record in hand, it can be passed to the remove() method of the appointmentsStore for removal.

We also define The disabled attribute with a value of true so that the button begins disabled and remains so until we enable it (when an item is selected).

The tasks detail section also includes a button to mark a task as complete, and that's a bit different from the Delete buttons:

new Ext.Button({
  text : "Complete Task", renderTo: "tdTaskDetailsCompleteButton",
  handler : function() {
    var viewMode = "IconView";
    if (Ext.getCmp("tbViewMode").getText() == "List View") {
      viewMode = "ListView";
    }
    var record = Ext.getCmp("dvTasks" + viewMode).getSelectedRecords()[0];
    record.set("status", DAO.TASK_STATUS_COMPLETE);
  }, disabled : true, id : "btnTaskCompleteButton"
});

It is only different in the code of the handler, and then it's only different in that at the end we call The set() method of the Record to set the status field. This causes the update event to fire on the data store, and the data is saved via a call to the DAO.

NOTE

At the end of The buildUI() method you'll also notice this statement: vp.doLayout();. This instructs the viewport essentially to draw itself. Typically you don't need to do this; it happens automatically. However, I noticed an issue in the latest version of Firefox (3.0.3 at the time of this writing) where the titles of the icon view and the Accordion pane wouldn't show up unless I issued this statement. It does no harm to do so generally, although it's probably inefficient and certainly redundant even if Ext JS is smart enough to not do any extra work it doesn't have to do. Just remember, you shouldn't usually have to do this, but I wanted to point out the reason I did.

3.5.2.22. Making the Accordion Work: Changing Categories

We're very nearly at the end of the code of this application. We've seen how the data stores and Record descriptors are created, and we've seen how the dialog Windows for creating new items are created. In addition, we've seen how the UI is built and how most of it works. Along the way, we saw calls to a few methods of the OrganizerExt class, and those methods are what remain to look at.

First, recall that when the user clicks a pane in The Accordion, the changeCategory() method is called:

this.changeCategory = function(inCategory) {

  organizerExt.currentCategory = inCategory;

  var newActiveItem = null;
  var listingTitle = null;
  var detailsTitle = null;
  switch (inCategory) {
    case "appointments":
      newActiveItem = 0;
      listingTitle = "Appointments";
      detailsTitle = "Appointment Details";
    break;
    case "notes":
      newActiveItem = 1;
      listingTitle = "Notes";
      detailsTitle = "Note Details";
    break;
    case "tasks":
      newActiveItem = 2;
      listingTitle = "Tasks";
      detailsTitle = "Task Details";
    break;
    case "contacts":
      newActiveItem = 3;
      listingTitle = "Contacts";
      detailsTitle = "Contact Details";
    break;
  }

  var listingCard = Ext.getCmp("listingCard");
  listingCard.setTitle(listingTitle);
  listingCard.getLayout().setActiveItem(newActiveItem);
  var detailsCard = Ext.getCmp("detailsCard");
  detailsCard.setTitle(detailsTitle);
  detailsCard.getLayout().setActiveItem(newActiveItem);
  organizerExt.changeViewMode(Ext.getCmp("tbViewMode").getText());

 }

Switching between categories entails switching the icon or list view, whichever mode the application is currently in, to the appropriate data store. That's what this method does, by and large. The inCategory argument is a string naming the category to switch to. The first thing we do is store the inCategory value in the currentCategory field of organizerExt, because we're going to need that value elsewhere.

So after that, we have a switch statement based on that argument. For each of the four possible values, we set three variables. The first, newActiveItem, will be the value of the activeItem attribute of the listingCard, the CardLayout containing the icon and list views. (If you're thinking ahead, you'll recall that there are eight cards in that CardLayout: four icon views followed by four list views... yet we only have four values here! Don't worry grasshopper, all will be revealed!)

The second variable is listingTitle, which is the text that will be displayed in the title bar above the listing section. Likewise, detailsTitle is the text to display in the title bar above the details section.

Once those variables are set, we move on to the common block of code following The switch block. First, we use the Ext.getCmp() method to get a reference to the listingCard, which is the CardLayout containing all four icon views and all four grid views. With that reference, we call setTitle() to set the title bar text. Next, we call the getLayout() method, which gives us a reference to the underlying ContainerLayout object for that Panel. This object exposes a setActiveItem() method, to which we pass the newActiveItem variable. This flips us over to the appropriate icon view for the category selected.

Next, we get a reference to the detailsCard, and likewise set its title and active item.

Now, at this point you've got to be saying to yourself, "Wait, what if we're currently in list view mode? Haven't we just flipped to an icon view?" Indeed we have, but that's where the call to organizerExt.changeViewMode() comes into play, which is our next destination.

3.5.2.23. Switching Between Icon View and List View

When users switch categories, or when they switch view modes, The changeViewMode() method gets called. In Figure 3-20 you can see an example of a switch to list view.

Figure 3.20. The list view mode

The code for this method is not terribly long, nor is it complex, as you can see for yourself:

this.changeViewMode = function(inMode) {

  var baseCardIndex = null;
  switch (organizerExt.currentCategory) {
    case "appointments":
      baseCardIndex = 0;
    break;
    case "notes":
      baseCardIndex = 1;
    break;
    case "tasks":
      baseCardIndex = 2;
    break;
    case "contacts":
      baseCardIndex = 3;
    break;
  }

var newActiveItem = null;
if (inMode == "List View") {
  newActiveItem = baseCardIndex + 4;
} else {
  newActiveItem = baseCardIndex;
}

var listingCard = Ext.getCmp("listingCard");
listingCard.getLayout().setActiveItem(newActiveItem);
}

Here, we're being passed a string that tells us what mode we're in. It happens to be one of the two text strings displayed on the mode switch button, so either List View or Icon View. The code begins by doing a switch based on the currentCategory, set back in the changeCategory() method. We do this to determine the index of the card in the CardLayout that is currently selected and store that in baseCardIndex. Since all four of the icon views were added first, we know that at this point the index value is either 0, 1, 2, or 3 (remember that in changeCategory() we always switched to an icon view, never a list view).

Next, we examine what view mode we're in. If we're currently in list view, we add four to the value of baseCardIndex. Think about that for a moment. The four icon views have an index value of 0, 1, 2, or 3. For example, the appointments icon view is index 0. The list views were added to the CardLayout right after the icon views, so that means they begin at index 4. So the appointments list view is at index 4. So by adding four to the baseCardIndex value, we now have the correct index for the list view associated with the current category. Of course, if we're in icon view mode, then baseCardIndex is the proper value already. In either case, the variable newActiveItem is set to the appropriate value.

Then, we set the active item in The CardLayout to the value of newActiveItem. If we are in icon view mode, then this effectively does nothing. But if we are in list view mode, we switch to the list view. The user only sees a single switch because it happens so fast.

3.5.2.24. Showing Details of a Selected Item

The very last thing we need to look at is how the details of a selected item are displayed. I'm definitely sounding like a broken record now, and I know it, but because all four of these methods (one each for notes, appointments, contacts, and tasks) are very similar, we only need to examine one. I'll pick on appointments one last time for this:

this.showAppointmentDetails = function(inRecord) {

  if (inRecord) {
    Ext.getCmp("btnAppointmentDeleteButton").enable();
  } else {
    Ext.getCmp("btnAppointmentDeleteButton").disable();
    inRecord = new organizerExt.AppointmentRecord({
      category : "", title : "", whendt : "", location : "", note : ""
    });
  }

Ext.getDom("appointment_category").innerHTML =
    inRecord.get("category");
  Ext.getDom("appointment_location").innerHTML =
    inRecord.get("location");
  Ext.getDom("appointment_note").innerHTML =
    inRecord.get("note");
  Ext.getDom("appointment_title").innerHTML =
    inRecord.get("title");
  var wdt = inRecord.get("whendt");
  Ext.getDom("appointment_whendt").innerHTML =
    Ext.isDate(wdt) ? inRecord.get("whendt").format("m/d/Y") : "";
 }

First, the code checks to see if the inRecord argument was null or not. This is to cover the case where the user clicks a blank section of the icon or list view, deselecting all items. In that case, the else branch would kick in. This disables the Delete button and creates a new Record of the appropriate type with all blank fields.

Then, it's a simple matter of setting the innerHTML attribute of each of the detail fields, which we get via calls to Ext.getDom() to the applicable fields from the Record object. If inRecord is null, it effectively clears all the fields since we set all the fields to blank.

The whendt field is slightly different. We want to display the value as mm/dd/yyyy, but that can only be done (with the Date.format() method at least) if it's a Date. If no record is available, however, it's a string. So, we get its value, and then use the Ext.isDate() method to determine if it's a Date. If it is, we can go ahead and format it (using the m/d/y specification, which outputs a value such as 10/21/2008); otherwise we just output an empty string.

And with that, our exploration of this code is complete!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.108.18