If we add this to the body section of our document, and change our effect to reference this instead of the body, then our effect will happen only to the my_id div we created: Ext.get('my_id').highlight('FF0000',{ endColor:'0000FF', duration: 3 }); If we now look at our document in a browser, we would see a 200-pixel square box changing color, instead of the entire body of the document changing color. Bear in mind that DOM element IDs must be unique. So once we have used my_id, we cannot use this ID again in our document. If duplicate IDs exist in our document, then our results will be unpredictable. When creating the DOM structure of widgets, Ext JS creates and tracks its own IDs. It is hardly ever necessary to create them on our own. One exception is when using HTML scaffolding—pre-existing HTML elements which are to be imported into Ext Components. Having duplicate IDs in our document can lead to strange behavior, such as a widgets always showing up in the upper-left corner of the browser, and must therefore be avoided. Minimizing memory usage If an element is not going to be repeatedly used, then we can avoid the caching mechanism which Ext.get uses to optimize element lookups. We can use something called a "flyweight" to perform simple tasks, which results in higher speed by not clogging up the browser's memory with cached elements which will not be needed again. Chapter 2 [ 39 ] The same highlight effect we just used could be written using a flyweight instead: Ext.fly('my_id').highlight('FF0000',{ endColor:'0000FF', duration: 3 }); This is used when we want to perform an action on an element in a single line of code, and we do not need to reference that element again. The flyweight re-uses the same memory over and over each time it is called. A flyweight should never be stored in a variable and used again. Here is an example of using a flyweight incorrectly: var my_id = Ext.fly('my_id'); Ext.fly('another_id'); my_id.highlight('FF0000',{ endColor:'0000FF', duration: 3 }); Because the flyweight re-uses the same memory each time it is called, by the time we run the highlight function on our my_id reference, the memory has changed to actually contain a reference to another_id. Can we use our own HTML? As mentioned earlier, pre-existing HTML elements may be "imported" into Ext JS widgets. The easiest way is to configure an Ext Panel with a contentEl. The following page creates a Panel which uses a pre-existing HTML element as its content by specifying the element's ID as the contentEl: The Staples of Ext JS [ 40 ]
This is some pre-existing content.
This element is "imported" and used as the body of a Panel
Another way to do this is to use the Ext.BoxComponent class to encapsulate an existing element of a page. The BoxComponent class is a very lightweight widget which simply manages an HTML DIV as an Ext widget. As mentioned, it will create its own HTML structure if required, but we can configure it to use an existing element as its main element. In the following example we use a Panel to contain the BoxComponent:
This is some pre-existing content.
This element is "imported" and used as the body of a Panel
Both the previous examples will produce the following output: The Staples of Ext JS [ 42 ] Summary Using only a few lines of code, we have created a fun program that will keep us entertained for hours! Well, maybe not for hours, but for at least a few minutes. Nonetheless, we have the beginnings of the basic functionality and user interface of a typical desktop application. We have learned the basics of using configuration objects, and I'm sure this will make even more sense after we have had the chance to play with more of the Ext JS widgets. But the real point here is that the configuration object is something that is very fundamental when using Ext JS. So the quicker we can wrap your heads around it, the better off we will be. Don't worry if you are not entirely comfortable with the configuration object yet. We have plenty of time to figure it out. For now, let's move on to one of my favorite things—forms. Forms In this chapter, we will learn how to create Ext JS forms, which are similar to the HTML forms that we use, but with a greater level of flexibility, error checking, styling, and automated layout. This functionality is encapsulated in the FormPanel class. This class is a panel because it extends the Panel class, and so it can do everything a panel can do. It also adds the ability to manage a set of input fields that make up the form. We will use some different form field types to create a form that validates and submits form data asynchronously. Then we will create a database-driven, drop-down Combo Box, and add some more complex field validation and masking. We will then finish it off with a few advanced topics that will give our forms some serious 'wow' factor. The goals of this chapter include: • Creating a form that uses AJAX submission • Validating field data and creating custom validation • Loading form data from a database The core components of a form A form has two main pieces, the functionality that performs form like actions, such as loading values and submitting form data, and the layout portion which controls how the fields are displayed. The FormPanel that we will be using combines these two things together into one easy-to-use Component, though we could use the form layout by itself if needed, which we will take a closer look at in Chapter 7. Forms [ 44 ] The possibilities are endless with Ext JS form fields. Key listeners, validation, error messages, and value restrictions are all built-in with simple config options. Customizing a form for our own specific needs can be done easily, which is something we will cover later on in this chapter. To start with, we will be adding fields to a standard form. Here are some of the core form components that we will become familiar with: • Ext.form.FormPanel: A specialized Panel subclass which renders a FORM element as part of its structure, and uses a form-specific layout to arrange its child Components. • Ext.form.BasicForm: The class which the FormPanel uses to perform field management, and which performs the AJAX submission and loading capabilities. • Ext.form.Field: A base class which implements the core capabilities that all form fields need. This base class is extended to create other form field types. Extending is a major part of the Ext JS architecture, and just means that common functionality is created in a single place and built upon (extended) to make more specific and complex components. Our first form To start with, let's create a form with multiple field types, a date field, validation, error messages, and AJAX submission—just a simple one for our first try. For this example, our fields will be created using an array of config objects instead of an array of instantiated Ext.form.Field components. This method will work just the same as an instantiated component, but will take less time to code. Another benefit which may come in handy is that a config object used to create items in this way may be stored and reused. A basic HTML page like the one we used in the previous example will be used as a starting point. The standard Ext JS library files need to be included and, as with everything we create in Ext JS, our code will need to be wrapped in the onReady function: Ext.onReady(function(){ var movie_form = new Ext.FormPanel({ url: 'movie-form-submit.php', renderTo: Ext.getBody(), frame: true, title: 'Movie Information Form', width: 250, items: [{ Chapter 3 [ 45 ] xtype: 'textfield', fieldLabel: 'Title', name: 'title' },{ xtype: 'textfield', fieldLabel: 'Director', name: 'director' },{ xtype: 'datefield', fieldLabel: 'Released', name: 'released' }] }); }); When we run this code in a browser, we end up with a form panel that looks like this: Nice form—how does it work? The FormPanel is very similar to an HTML form. It acts as the container for our form fields. Our form has a url config so the form knows where to send the data when it is submitted. As it is a Component (inherits from the Component base class), it also has a renderTo config, which defines into which existing element the form is appended upon creation. As we start to explore layouts in Chapter 7, the renderTo config will be used much less often. Child items The items config option is an important one as it specifies all of our child Components—in this case they are all input fields. The ability to house child Components is inherited from the Container level of the Component family tree—our FormPanel in this case. The items config is an array which may reference either component config objects, or fully created Components. In our example, each array element is a config object which has an xtype property that defines which type of Ext JS component will be used: text, date, number, or any of the many others. This could even be a grid or some other type of Ext JS component. Forms [ 46 ] Be aware that the default xtype is a panel. Any {…} structure with no xtype property, as an element in an items config, results in a panel at that position. But where do xtypes come from, and how many of them are there? An xtype is just a string key relating to a particular Ext JS Component class, so a 'textfield' xtype refers to its Ext.form.TextField counterpart. Here are examples of some of the form field xtypes that are available to us: • textfield • timefield • numberfield • datefield • combo • textarea The key point to remember is that child items may be any class within the Component class hierarchy. We could easily be using a grid, toolbar, or button—pretty much anything! (Though getting and setting their values is a different story.) This is an illustration of the power of the Component class hierarchy mentioned in Chapter 2. All Ext JS component classes, because they share the same inheritance, can be used as child items, and be managed by a Container object—in this case a form. Our basic field config is set up like this: { xtype: 'textfield', fieldLabel: 'Title', name: 'title' } Of course, we have the xtype that defines what type of a field it is—in our case it is a textfield. The fieldLabel is the text label that is displayed to the left of the field, although this can also be configured to display above the field. The name config is just the same as its HTML counterpart and will be used as the parameter name when sending form data to the server. The names of most of the config options for Ext components match their counterparts in HTML. This is because Ext was created by web developers, for web developers. Creating the subsequent date field isn't much different from the text field we just made. Change the xtype from textfield to datefield, update the label and name, and we're done. Chapter 3 [ 47 ] { xtype: 'datefield', fieldLabel: 'Released', name: 'released' } Validation A few of our sample fields could have validations that present the users with error indicators if the user does something wrong, such as leaving a field blank. Let's add some validation to our first form. One of the most commonly-used types of validation is checking to see if the user has entered any value at all. We will use this for our movie title field. In other words, let's make this field a required one: { xtype: 'textfield', fieldLabel: 'Title', name: 'title', allowBlank: false } Setting up an allowBlank config option and setting it to false (the default is true) is easy enough. Most forms we build will have a bunch of required fields just like this. Each type of Ext JS field also has its own set of specialized configurations that are specific to the data type of that field. The following are some examples of the options available: Field Type Option Value type Description numberfield decimalPrecision Integer How many decimal places to allow datefield disabledDates Array An array of date strings that cannot be selected timefield increment Integer How many minutes between each time option For instance, a date field has ways to disable certain days of the week, or to use a regular expression to disable specific dates. The following code disables every day except Saturday and Sunday: { xtype: 'datefield', fieldLabel: 'Released', name: 'released', disabledDays: [1,2,3,4,5] } Forms [ 48 ] In this example, every day except Saturday and Sunday is disabled. Keep in mind that the week starts on 0 for Sunday, and ends on 6 for Saturday. When we use other types of fields, we have different validations, like number fields that can restrict the size of a number or how many decimal places the number can have. The standard configuration options for each field type can be found in the API reference. There are many more configuration options, specific to each specialized Ext JS Component class. To easily find what options are available in the online documentation, use the Hide inherited members button at the top right. This will hide config options from the base classes of the Component's heritage, and only show you options from the Component you are interested in: Chapter 3 [ 49 ] Built-in validation—vtypes Another more flexible type of validation is the vtype. This can be used to validate and restrict user input, along with reporting back error messages. It will work in just about any scenario you can imagine because it uses a function and regular expressions to do the grunt work. Here are some built-in vTypes that can come in handy: • email • url • alpha • alphanum These built-in vtypes come included by default, and we can use them as a starting point for creating our own vtypes, which we will cover in the next section. The following is an example of an alpha vtype being used on a text field: Ext.onReady(function(){ var movie_form = new Ext.FormPanel({ url: 'movie-form-submit.php', renderTo: document.body, frame: true, title: 'Movie Information Form', width: 250, items: [{ xtype: 'textfield', fieldLabel: 'Title', name: 'title', allowBlank: false },{ xtype: 'textfield', fieldLabel: 'Director', name: 'director', vtype: 'alpha' },{ xtype: 'datefield', fieldLabel: 'Released', name: 'released', disabledDays: [1,2,3,4,5] }] }); }); Forms [ 50 ] All we did was add a vtype to the director field. This will validate that the value entered is composed of only alphabet characters. Now we're starting to see that the built-in vtypes are very basic. The built-in alpha vtype restricts our fields to alphabet characters only. In our case, we want the user to enter a director's name, which would usually contain only alphabetic characters, with just one space between the first and last names. Capitalizing the first characters in the names could possibly make them look pretty. We will create our own custom vtype soon enough, but first let's take a look at displaying error messages. A search of the Ext JS forum is likely to come back with a vType that someone else has created that is either exactly what you need, or close enough to use as a starting point for your own requirements. Styles for displaying errors Forms are set up by default with a very bland error display which shows any type of error with a squiggly red line under the form field. This error display closely mimics the errors shown in programs like Microsoft Word when you spell a word incorrectly. We do have other options for displaying our error messages, but we will need to tell Ext JS to use them instead of the default. One built-in option is to display the error message in a balloon using an Ext JS object called QuickTips. This utilizes the standard squiggly line, but also adds a balloon message that pops up when we mouse over the field, displaying error text within. Chapter 3 [ 51 ] We just need to add one line of code before our form is created that will initialize the QuickTips object. Once the QuickTips object is initialized, the form fields will automatically use it. This is just one simple statement at the beginning of the script: Ext.QuickTips.init(); This is all that needs to happen for our form fields to start displaying error messages in a fancy balloon. The QuickTips object can also be used to display informational tool tips. Add a qtip attribute which contains a message to any DOM node in the document, and when we hover the mouse over that element, a floating message balloon is displayed. Without the red "error" styling shown above. Custom validation—creating our own vtype In this section we will create a custom vtype to perform validation based upon matching the input field's value against a regular expression. Don't be afraid to experiment with regular expressions. They simply match characters in a string against a series of patterns which represent classes of characters, or sequences thereof. Our vtype will check that the input consists of two words, each of which begins with a capital letter, and is followed by one or more letters. A hint for working with regular expressions: use the browser's JavaScript debugging console to test regular expressions against strings until you come up with one that works. To create our own vtype, we need to add it to the vtype definitions. Each definition has a value, mask, error text, and a function used for testing: • xxxVal: This is the regular expression to match against • xxxMask: This is the masking to restrict user input • xxxText: This is the error message that is displayed Forms [ 52 ] As soon as we figure out the regular expressions we need to use, it's fairly straightforward to create our own vType—so let's try one out. Here is a validation for our director's name field. The regular expression matches a pair of alpha strings, separated by a space, and each starting with a capital letter. Sounds like a good way to validate a name—right? The Ext.apply function is just a simple way of copying properties from one object into another—in this case from an object literal which we create, into the Ext.form.VTypes object. Ext.apply(Ext.form.VTypes, { nameVal: /^[A-Z][A-Za-z]+\s[A-Z][A-Za-z]+$/, nameMask: /[A-Za-z ]/, nameText: 'Invalid Director Name.', name: function(v) { return this.nameVal.test(v); } }); It's hard to look at this all at once, so let's break it down into its main parts. We first start with the regular expression that validates the value entered into our form field. In this case it's a regular expression used to test the value: nameVal: /^[A-Z][A-Za-z]+\s[A-Z][A-Za-z]+$/, Next, we add the masking, which defines what characters can be typed into our form field. This is also in the form of a regular expression: nameMask: /[A-Za-z ]/, Then, we have the text to be displayed in a balloon message if there is an error: nameText: 'Invalid Director Name.', And finally, the part that pulls it all together—the actual function used to test our field value: name: function(v) { return this.nameVal.test(v); } Put all this together and we have our own custom vtype without much effort —one that can be used over and over again. We use it in just the same way as the 'alpha' vtype: vtype: 'name' Chapter 3 [ 53 ] The result should look like this: Even though our example used a regular expression to test the value, this is not the only way vTypes can work. The function used can perform any type of comparison we need, and simply return true or false. Masking—don't press that key! Masking is used when a particular field is forced to accept only certain keystrokes, such as numbers only, or letters, or just capital letters. The possibilities are limitless, because regular expressions are used to decide what keys to filter out. This mask example would allow an unlimited string of only capital letters: { xtype: 'textfield', ... maskRe: /[A-Z]/, ... } Instead of using the masking config, consider creating a vType to accomplish your masking. If the formatting requirements should happen to change, it will be centrally-located for easy updating. So when the day arrives where your boss comes to you freaking out and tells you, "Remember those product codes that I said would always be ten numbers, well it turns out they will be eight letters instead", you can make the change to your vType, and go play Guitar Hero for the rest of the day! Radio buttons and check boxes Radio buttons and check boxes can be clumsy and hard to work with in plain HTML. However, creating a set of radio buttons to submit one of a limited set of options is easy in Ext JS. Let's add a RadioGroup component to the form which submits the film type of the movie being edited. Forms [ 54 ] It's not a button, it's a radio button The code below adds a set of radio buttons to our form by using the 'radiogroup' xtype. We configure this RadioGroup to arrange the buttons in a single column. The default xtype of items in a RadioGroup is 'radio' so we do not need to specify it: { xtype: 'radiogroup', columns: 1, fieldLabel: 'Filmed In', name: 'filmed_in', items: [{ name: 'filmed_in', boxLabel: 'Color', inputValue: 'color' },{ name: 'filmed_in', boxLabel: 'Black & White', inputValue: 'B&W' }] } The RadioGroup's name allows the form to be loaded using a single property value. The name in the individual button configs allows submission of the selected inputValue under the correct name. These radio buttons work much like their HTML counterparts. Give them all the same name, and they will work together for you. The additional config for boxLabel is needed to show the informative label to the right of the input. X marks the checkbox Sometimes, we need to use checkboxes for Boolean values—sort of an on/off switch. By using the checkbox xtype, we can create a checkbox. Chapter 3 [ 55 ] { xtype: 'checkbox', fieldLabel: 'Bad Movie', name: 'bad_movie' } Both the radio and checkbox have a 'checked' config that can be set to true or false to check the box upon creation. { xtype: 'checkbox', fieldLabel: 'Bad Movie', name: 'bad_movie', checked: true } Sets of checkboxes can be arranged in columns by containing them in a CheckboxGroup. This is configured in exactly the same way that a RadioGroup is, but the default xtype of its child items is 'checkbox'. The ComboBox The ComboBox class emulates the functionality of the HTML SELECT element. It provides a dropdown box containing a list from which one value can be selected. It goes further than simply imitating a SELECT box though. It has the ability to either provide its list from a loaded data store, or, it can dynamically query the server during typing to load the data store or demand to produce autosuggest functionality. First, let's make a combo using local data from a loaded data store. To do this, the first step is to create a data store. A data store is a client-side analogue of a database table. It encapsulates a set of records, each of which contains a defined set of fields. Full details about data stores are contained in Chapter 15. There are a few different types of data stores, each of which can be used for different situations. However, for this one, we are going to use an Array store, which is one of the shortcut data classes we will cover in more detail in both the Grids and Data chapters. The ArrayStore class is configured with the field names which its records are to contain and a two-dimensional array which specifies the data: var genres = new Ext.data.ArrayStore({ fields: ['id', 'genre_name'], data : [['1','Comedy'],['2','Drama'],['3','Action']] }); Forms [ 56 ] A few new config options are needed when we are setting up a combo box. • store: This is the obvious one. The store provides the data which backs up the combo. The fields of its records provide the descriptive text for each dropdown item, and the value which each item represents. • mode: This option specifies whether the combo expects the data store to be pre-loaded with all available items ('local'), or whether it needs to dynamically load the store, querying the server based upon the typed characters ('remote'). • displayField: This option specifies the field name which provides the descriptive text for each dropdown item. • valueField: This option specifies the field name which provides the value associated with the descriptive text. This is optional. If used, the hiddenName option must be specified. • hiddenName: This is the name of a hidden HTML input field which is used to store and submit the separate value of the combo if the descriptive text is not what is to be submitted. The visible field will contain the displayField. Just like the other fields in our form, we add the combo to our items config: { xtype: 'combo', hiddenName: 'genre', fieldLabel: 'Genre', mode: 'local', store: genres, displayField:'genre_name', valueField:'id', width: 120 } This gives us a combo box that uses local, pre-loaded data, which is good for small lists, or lists that don't change often. What happens when our list needs to be pulled up from a database? A quick way to specify a few static options for a ComboBox is to pass an array to the store config. So if we wanted a ComboBox that had 'Yes' and 'No' as options, we would provide ['Yes','No'] as the store config value. Chapter 3 [ 57 ] A database-driven ComboBox The biggest change that needs to happen is on the server side—getting our data and formatting it into a JSON string that the combo box can use. Whatever server-side language is used, we will need a JSON library to 'encode' the data. If we're using PHP 5.1 or higher, this is built in. To check our version of PHP, we can either execute a command in a terminal window or run a single line of PHP code. If we have access to this command line we can run php –v to check our version, otherwise, running a script that just has the single line will do the job. This is what we would use to generate our JSON data using PHP 5.1 or higher: 0) { while ($obj = mysql_fetch_object($result)) { $arr[] = $obj; } } Echo '{rows:'.json_encode($arr).'}'; ?> When we use remote data, there are a few more things that need to happen back on the JavaScript side. First, the data store needs to know what format the data is in. When we use shortcut store classes like ArrayStore, this is implicit (It's an array). In this example, we specify the format of the data being read by using a data reader—in our case, it's the JSON Reader. var genres = new Ext.data.Store({ reader: new Ext.data.JsonReader({ fields: ['id', 'genre_name'], root: 'rows' }), proxy: new Ext.data.HttpProxy({ url: 'data/genres.php' }) }); Forms [ 58 ] The data reader is configured using two config options: • fields: This is an array of the field names which the records in the store will contain. For a JSON Reader, these names are also the property names within each row data object from which to pull the field values. • root: This specifies the property name within the raw, loaded data object which references the array of data rows. Each row is an object. We can see the root and field properties specified in the configuration in this example of the returned JSON data: {rows:[ { "id":"1", "genre_name":"Comedy", "sort_order":"0" },{ "id":"2", "genre_name":"Drama", "sort_order":"1" },{ // snip...// }] } We have also set up the proxy, a class which takes responsibility for retrieving raw data objects. Typically this will be an HttpProxy that retrieves data from a URL at the same domain as the web page. This is the most common method, but there is also a ScriptTagProxy that can be used to retrieve data from a different domain. All we need to provide for our proxy is the URL to fetch our data from. Whenever we specify a HttpProxy we are actually using Ajax. This requires that we have a web server running; otherwise Ajax will not work. Simply running our code from the file system in a web browser will not work. Let's throw in a call to the load function at the end, so the data is loaded into our combo box before the user starts to interact with it. genres.load(); Chapter 3 [ 59 ] This gives us a combo box that's populated from our database, and should look like this: Another way to pre-load the data store is to set the autoLoad option to true in our data store configuration: var genres = new Ext.data.Store({ reader: new Ext.data.JsonReader({ fields: ['id', 'genre_name'], root: 'rows' }), proxy: new Ext.data.HttpProxy({ url: 'data/genres.php' }), autoLoad: true }); TextArea and HTMLEditor We are going to add a multiline text area to our movie information form, and Ext JS has a couple of options for this. We can either use the standard textarea that we are familiar with from using HTML, or we can use the HTMLEditor field, which provides simplistic rich text editing: • textarea: Similar to a typical HTML textarea field • htmleditor: A rich text editor with a button bar for common formatting tasks We will use a couple of new config options for this input: • hideLabel: This causes the form to not display a label to the left of the input field, thus allowing more horizontal space for the field. • anchor: This config is not a direct config option of the Field class. It is a "hint" to the Container (in this case a FormPanel) that houses the field. It anchors the child item to the right or bottom borders of the container. A single value of '100%' anchors to the right border, resulting in a full-width child component. Forms [ 60 ] { xtype: 'textarea', name: 'description', hideLabel: true, height: 100, anchor: '100%' } By changing just the xtype, as shown below, we now have a fairly simple HTML editor with built-in options for font face, size, color, italics, bold, and so on: { xtype: 'htmleditor', name: 'description', hideLabel: true, height: 100, anchor: '100%' } The HtmlEditor can now be seen occupying the full panel width at the bottom of the panel: Listening for form field events Ext JS makes it extremely simple to listen for particular events in a Component's lifecycle. These may include user-initiated events such as pressing a particular key, changing a field's value, and so on. Or they may be events initiated by code, such as the Component being rendered or destroyed. Chapter 3 [ 61 ] To add event handling functions (which we call "listeners") to be called at these points, we use the listeners config option. This option takes the form of an object with properties which associate functions with event names. The events available to the listeners config option of a Component are not the same as the standard native DOM events which HTML offers. Some Components do offer events of the same name (for example a click event), but that is usually because they "add value" to the native DOM event. An example would be the click event offered by the Ext JS tree control. This passes to the listener the tree node that was clicked on. Adding our own listeners to DOM events of the constituent element of a Component is explained later in this chapter. A common task would be listening for the Enter key to be pressed, and then submitting the form. So let's see how this is accomplished: { xtype: 'textfield', fieldLabel: 'Title', name: 'title', allowBlank: false, listeners: { specialkey: function(field, eventObj){ if (eventObj.getKey() == Ext.EventObject.ENTER) { movie_form.getForm().submit(); } } } } The specialkey event is fired whenever a key related to navigation or editing is pressed. This includes arrow keys, Tab, Esc, and Enter. When an event is fired, the listener function associated with that event name is called. We want to check to see if it was the Enter key that was pressed before we take action, so we're using the event object—represented as the variable 'eventObj' in this example—to find out what key was pressed. The getKey method tells us which key was pressed, and the Ext.EventObject .ENTER is a constant that represents this key. With this simple listener and if statement, the form will be submitted when we press the Enter key. Forms [ 62 ] ComboBox events Combo boxes commonly need to have events attached to them. Let's take our genre combo box and attach a listener to it that will run when an item in the list is selected. First let's add a dummy item to our data as the first item in the list and call it New Genre: var genres = new Ext.data.SimpleStore({ fields: ['id', 'genre_name'], data : [ ['0','New Genre'], ['1','Comedy'], ['2','Drama'], ['3','Action'] ] }); Then, we add the listener to our combo: { xtype: 'combo', name: 'genre', fieldLabel: 'Genre', mode: 'local', store: genres, displayField:'genre_name', width: 130, listeners: { select: function(field, rec, selIndex){ if (selIndex == 0){ Ext.Msg.prompt('New Genre', 'Name', Ext.emptyFn); } } } } The listeners object in the code above specifies a function which is to be called when a combo item is selected. Each event type has its own set of parameters which are passed to listener functions. These can be looked up in the API reference. For the select event, our function is passed three things: • The form field • The data record of the selected combo item • The index number of the item that was clicked on Chapter 3 [ 63 ] Inside our listener function, we can see which item in the list was selected. The third argument in our listener function is the index of the item that was clicked. If that has an index of zero (the first item in the list), then we will prompt the user to enter a new genre using the prompt dialog we learned about in the previous chapter. The result should look like this: A list of valid events to listen for can be found at the bottom of the API documentation page for each Component, along with the arguments the listeners are passed, which are unique to each event. Buttons and form action Now, we have quite a complex form with only one problem—it doesn't send data to the server, and we will want a way to reset the form, which was the actual point behind creating our form in the first place. To do this, we are going to add some buttons which will perform these actions. Our buttons are added to a buttons config object, similar to the way that the form fields were added. These buttons really only need two things: the text to be displayed on the button, and the function (which is called the handler) to execute when the button is clicked. buttons: [{ text: 'Save', handler: function(){ movie_form.getForm().submit({ success: function(form, action){ Ext.Msg.alert('Success', 'It worked'); Forms [ 64 ] }, failure: function(form, action){ Ext.Msg.alert('Warning', 'Error'); } }); } }, { text: 'Reset', handler: function(){ movie_form.getForm().reset(); } }] The handler is provided with a function—or a reference to a function—that will be executed once the button is clicked. In this case, we are providing an anonymous function. Form submission Our FormPanel has a url option that contains the name of the file that the form data will be sent to. This is simple enough—just like an HTML form, all of our fields will be posted to this url, so they can be processed on the server side. movie_form.getForm().submit({ success: function(form, action){ Ext.Msg.alert('Success', 'It worked'); }, failure: function(form, action){ Ext.Msg.alert('Warning', 'Error'); } }); Inside our Save button, we have an anonymous function that runs the following code. This will run the actual submission function for our form, which sends the data to the server using AJAX. No page refresh is needed to submit the form. It all happens in the background, while the page you are looking at remains the same: In order for our form submission to work properly, the HTML page must be run from a web server, not the file system. The success and failure options provided to the submit call handle the server's response. These are also anonymous functions, but could just as easily be references to functions created earlier on in the code. Chapter 3 [ 65 ] Did you notice that the functions have a pair of arguments passed to them? These will be used to figure out what response the server gave. But first, we need to discuss how to provide that response on the server side. Talking back—the server responses When our form is submitted to the server, a script on the server side will process the post data from the form, and decide if a true or false 'success' message should be sent back to the client side. Error messages can be sent back along with our response, and these can contain messages that correspond to our form field names. When using forms and server-side validation, a success Boolean value is required. An example of a response JSON string from the server would look like this: { success: false, errors: { title: "Sounds like a Chick Flick" } } When the success property is set to false, it triggers the Ext JS form to read in the error messages from the errors property, and apply them to the form's validation to present the user with error messages. Server-side validation of our form submission gives us a way to look up information on the server side, and return errors based on this. Let's say we have a database of bad movie names, and we don't want users to submit them to our database. We can submit the form to our script, which checks the database and returns a response based on the database lookup of that name. If we wanted to filter out chick flicks the response could look something like this: { success: false, errors: { title: "Sounds like a Chick Flick" }, errormsg: "That movie title sounds like a chick flick." } The false success response triggers the form's error messages to be displayed. An errors object is passed with the response. The form uses this object to determine each of the error messages for the fields. A name/value pair exists in the errors object for each form field's error. Forms [ 66 ] Our example response also passes an errormsg property, which is not used by the form, but is going to be accessed separately to present our own error message in an Ext JS Message Box. The error objects messages are handled automatically by the form, so let's take the extra error message that we were passing back, and display it in a message box. buttons: [{ text: 'Save', handler: function(){ movie_form.getForm().submit({ success: function(form, action){ Ext.Msg.alert('Success', 'It worked'); }, failure: function(form, action){ Ext.Msg.alert('Warning', action.result.errormsg); } }); } }, { text: 'Reset', handler: function(){ movie_form.getForm().reset(); } }] Our submit form action passes information back to the success and failure handlers. The first argument is the Ext JS form Component we were using, and the second is an Ext JS action object. Let's take a look at what's available in the Ext JS action object: Option Data type Description failureType String The type of failure encountered, whether it was on the client or server response Object Contains raw information about the server's response, including useful header information result Object Parsed JSON object based on the response from the server type String The type of action that was performed–either submit or load Chapter 3 [ 67 ] Now that we know what is available to the failure handler, we can set up some simple error checking: failure: function(form, action){ if (action.failureType == Ext.form.Action.CLIENT_INVALID) { Ext.Msg.alert("Cannot submit", "Some fields are still invalid"); } else if (action.failureType === Ext.form.Action.CONNECT_FAILURE) { Ext.Msg.alert('Failure', 'Server communication failure: '+ action.response.status+' '+action.response.statusText); } else if (action.failureType === Ext.form.Action.SERVER_INVALID) { Ext.Msg.alert('Warning', action.result.errormsg); } } } By checking the failure type, we can determine if there was a server connection error and act accordingly, even providing details about the server's specific error message by inspecting the result and response properties. Loading a form with data There are three basic ways in which forms are used in a user interface: • To input data for a separate action—say, Google search • To create new data • To change existing data It's the last option is what we are interested in now. To accomplish this, we need to learn how to load that data from its source (static or database) into our user interface—our form. Static data load We can take data from somewhere in our code, a variable for instance, or just plain static text, and display it as the value in our form field by calling the setValue method. This single line of code will set a field's value: movie_form.getForm().findField('title'). setValue('Dumb & Dumber'); Forms [ 68 ] Once we start working with larger forms with many fields, this method becomes a hassle. That's why we also have the ability to load our data in bulk via an AJAX request. The server side would work much as it did when we loaded the combo box: 0) { $obj = mysql_fetch_object($result); Echo '{success: true, data:'.json_encode($obj).'}'; }else{ Echo '{success: false}'; } ?> This would return a JSON object containing a success property, and a data object that would be used to populate the values of the form fields. The returned data would look something like this: { success: true, data:{ "id":"1", "title":"Office Space", "director":"Mike Judge", "released":"1999-02-19", "genre":"1", "tagline":"Work Sucks", "coverthumb":"84m.jpg", "price":"19.95", "available":"1" } } To trigger this, we need to use the form's load method: movie_form.getForm().load({ url:'data/movie.php', params:{ id: 1 } }); Chapter 3 [ 69 ] Providing the load method with a url and params config will do the trick. The params config represents what is sent to the server side script as post/get parameters. By default, these are sent as post parameters. DOM listeners As mentioned when discussing adding listeners to Components, adding DOM event listeners to the constituent HTML elements of a Component is a different matter. Let's illustrate this by adding a click listener to the header of the FormPanel. When a panel is configured with a title, an element which houses that title is created when the Component is rendered. A reference to that element is stored in the panel's header property. The key point here is that no elements exist until the Component is rendered. So we use a listener function on the render lifecycle event to add the click listener: var movie_form = new Ext.FormPanel({ url: 'movie-form-submit.php', renderTo: document.body, frame: true, title: 'Movie Information Form', listeners: { render: fuction(component) { component.header.addListener({ click: function(eventObj, el) { Ext.Msg.alert("Click event", "Element id " + el.id + " clicked"); } }); } } Let's take the time to read and understand that code. We specify a listener function for the render event. The listener function is passed as a reference to the Component which just rendered. At this point, it will have a header property. This is an Ext.Element object which was talked about in Chapter 2. We call the addListener method of the Ext.Element class to add our click listener function. The parameter to this class should be familiar to you. It is a standard listeners config object which associates an event name property with a listener function which is called on every mouse click. Forms [ 70 ] Summary We have taken the foundation of the classic web application—forms—and injected them with the power of Ext JS, creating a uniquely-flexible and powerful user interface. The form created in this chapter can validate user input, load data from a database, and send that data back to the server. From the methods outlined in this chapter, we can go on to create forms for use in simple text searches, or a complexly validated data entry screen. Menus, Toolbars, and Buttons The unsung heroes of every application are the simple things like buttons, menus, and toolbars. In this chapter, we will cover how to add these items to our applications. By following the examples in this chapter we will learn how to use menus, both as components in their own right—either static or floating as popups—and as dependent menus of buttons. We will then learn how to use toolbars, which contain buttons that call a function on click, or that pop up a submenu on click, or cycle between several submenu options on each click. The primary classes we will cover in this chapter are: • Ext.menu.Menu: A Container class which by default displays itself as a popup component, floating above all other document content. A menu's child items behave in a similar manner to buttons, and may call a handler function on mouse click. A menu may also be used as a static component within a page. • Ext.Toolbar: A Container class which arranges its child Components horizontally in the available width, and manages overflow by offering overflowed Components in a popup menu. • Ext.Button: The primary handler for button creation and interaction. A Component class which renders a focusable element which may be configured with a handler function which is called upon mouse click, or a menu to display upon mouse click. • Ext.SplitButton: A subclass of button which calls a handler function when its main body is clicked, but also renders an arrow glyph to its right which can display a dropdown menu when clicked. Menus, Toolbars, and Buttons [ 72 ] • Ext.CycleButton: A subclass of SplitButton which cycles between checking individual menu options of its configured menu on each click. This is similar to cycling through different folder views in Windows Explorer. • Ext.ButtonGroup: A Panel class which lays out child Components in a tabular format across a configurable number of columns. What's on the menu? We will begin by introducing the Menu class which will be used in all following examples. We are going to demonstrate usage of the Menu class as both a static component within a page, and as a popup. Both menus will be configured with the same options by using a technique which was suggested in Chapter 2: we define a variable called menuItems to reference an array which specifies the menu's items, and use it in both cases. The Menu class inherits from Container, so any menu options are child Components specified in the items config. It also inherits the usual Component config options such as renderTo, and the width option. The static menu will be rendered to the document body, and in order for it to be rendered as a visible, static element in the document, we configure it with floating: false. So the configuration we end up with is as follows: new Ext.menu.Menu({ renderTo: document.body, width: 150, floating: false, items: menuItems }); The popup menu needs no extra configuring aside from its items. We do need to decide when and where to display it. In this case we will add a contextmenu (right click) event listener to the document, and show the menu at the mouse event's position: var contextMenu = new Ext.menu.Menu({ items: menuItems }); Ext.getDoc().on({ contextmenu: function(eventObj) { contextMenu.showAt(eventObj.getXY()); Chapter 4 [ 73 ] }, stopEvent: true }); When we run this example, the static menu will be visible. When we right click on the document, the result should be the two menus shown below. Notice how only the second, popup menu has a shadow to indicate that it floats above the document. The menu's items The menuItems variable references an array which should be familiar by now. Just like the items config of a FormPanel used in the previous chapter, it's a list of child Components or config objects. In a menu, a config object with no xtype creates a MenuItem Component. The MenuItem class accepts the following config options in addition to those it inherits: • icon: The URL of an image to display as an icon • iconCls: A CSS class name which allows a stylesheet to specify a background image to use as an icon • text: The text to display • handler: A function to call when the item is clicked • menu: A Menu object, or Menu configuration object or an array of menu items to display as a submenu when the item is clicked Because a menu inherits from Container, it can accept other Components as child items. If some complex, menu option dependent input is required, a menu may be configured with a panel as a child item. The menu config of "Menu Option 2" we're creating next contains a FormPanel as its sole child item: { text: 'Menu Option 2', iconCls: 'flag-green', menu: { Menus, Toolbars, and Buttons [ 74 ] plain: true, items: { xtype: 'form', border: false, bodyStyle: 'background:transparent;padding:5px', labelWidth: 70, width: 300, defaults: { anchor: '100%' }, items: [{ xtype: 'combo', editable: false, fieldLabel: 'Select', triggerAction: 'all', store: [ [0, 'One or...'], [1 ,'The other']], value: 0, getListParent: function() { return this.el.up('div.x-menu'); } }, { xtype: 'textfield', fieldLabel: 'Title' }], fbar: [{ text: 'Submit' }] } } } The configurations in the above object will mostly be familiar by now. There is one extra config we use for the menu which contains the FormPanel. • plain: Specify as true so that the menu does not have to show the incised line for separating icons from text The panel within the menu has the following configs: border: Specify as false to produce a panel with no borders. bodyStyle: A CSS style string to apply to the document body. We want to make it transparent to allow the menu to show, and we apply padding. Chapter 4 [ 75 ] The ComboBox must render its dropdown list to the menu's element so that clicking on the list does not trigger the menu to hide: • GetListParent: This is a function which a ComboBox may be configured with. It must return the HTML element to render the dropdown list into. By default a ComboBox renders its dropdown into the document. We call the up function of the Ext.Element class to find the ancestor node of the combo's element which is a DIV which has the CSS class "x-menu". The FormPanel as a child of a menu will display like this: A toolbar for every occasion An Ext JS Panel, and every Ext JS Component which inherits from the Panel class (This includes Window, TreePanel, and GridPanel) can be configured to render and manage a toolbar docked above, or below the panel's body—or both if really necessary. These are referred to as the top and bottom toolbars, or tbar and bbar for short. Panels and subclasses thereof may also be configured with a footer bar which renders buttons right at the bottom of the panel—below any bottom toolbar. The Toolbar class is also an Ext JS Component in its own way, and may when necessary be used on its own, or as a child Component of any Container. Our second example renders a toolbar standalone into the body of the document. We will use all the main button types to illustrate their usage before moving on to add handlers to react to user interaction. The toolbar will contain the following child components: • A basic button • A button configured with a menu which is displayed when the button is clicked • A SplitButton which will display a menu only when its arrow glyph is clicked • A CycleButton which on click, cycles between three different options • A pair of mutually exclusive toggle buttons of which only one may be in a "pressed" state at once Menus, Toolbars, and Buttons [ 76 ] Ext.onReady(function(){ new Ext.Toolbar({ renderTo: Ext.getBody(), items: [{ xtype: 'button', text: 'Button' },{ xtype: 'button', text: 'Menu Button', menu: [{ text: 'Better' },{ text: 'Good' },{ text: 'Best' }] },{ xtype: 'splitbutton', text: 'Split Button', menu: [{ text: 'Item One' },{ text: 'Item Two' },{ text: 'Item Three' }] }, { xtype: 'cycle', showText: true, minWidth: 100, prependText: 'Quality: ', items: [{ text: 'High', checked: true }, { text: 'Medium' }, { text: 'Low' }] }, { text: 'Horizontal', toggleGroup: 'orientation-selector' }, { text: 'Vertical', Chapter 4 [ 77 ] toggleGroup: 'orientation-selector' }] }); }); As usual, everything is inside our onReady event handler. The items config holds our toolbar's entire child Components—I say child Components and not buttons because as we now know, the toolbar can accept many different types of Ext JS Components including entire forms or just form fields—which we will be implementing later on in this chapter. The result of the above code looks like this: The default xtype for each element in the items config is button. We can leave out the xtype config element if button is the type we want, but I like to include it just for clarity. Button configuration In addition to inherited config options, a button accepts the following configurations which we will be using in the following examples for this chapter: • icon: The URL of an image to display as an icon • iconCls: A CSS class name which allows a stylesheet to specify a background image to use as an icon • text: The text to display • handler: A function to call when the button is clicked • menu: A Menu object, or Menu configuration object, or an array of menu items to display as a submenu when the button is clicked • enableToggle: Specify as true to make a single button toggleable between pressed and unpressed state • toggleGroup: A mnemonic string identifying a group of buttons of which only one may be in a "pressed" state at one time • toggleHandler: A function to be called when a button's "pressed" state is changed Menus, Toolbars, and Buttons [ 78 ] A basic button Creating a button is fairly straightforward; the main config option is the text that is displayed on the button. We can also add an icon to be used alongside the text if we want to. A handler function is called when the button is clicked. Here is the most basic configuration of a button: { xtype: 'button', text: 'Button', handler: functionReference } The following screenshot shows what happens when the mouse is hovered over the Button button: Button with a menu A button may be configured to act as a trigger for showing a dropdown menu. If configured with a menu option, clicking the button displays a menu below the button. The alignment of the menu is configurable, but defaults to being shown below the button. Each option within the menu may itself be configured with a menu option allowing a familiar cascading menu system to be built very easily. The following is a config for a button which displays a dropdown menu upon click: { xtype: 'button', text: 'Button', menu: [{ text: 'Better' },{ text: 'Good' },{ text: 'Best' }] } The following screenshot shows what happens when the Menu Button is clicked on, and the mouse is hovered over the Best option: Chapter 4 [ 79 ] Split button A split button may sound like a complex component, but it is no more complex to create than a plain button. By using a split button we get the ability to specify a click handler which is called when the main body of the button is clicked. But we can also configure in a menu which will be displayed when the arrow glyph to the right of the main body is clicked. { xtype: 'split', text: 'Split Button', menu: [{ text: 'Item One' },{ text: 'Item Two' },{ text: 'Item Three' }] } The following screenshot shows what happens when the Split Button's arrow glyph is clicked: Toggling button state Sometimes it is useful to have a button which "sticks" in the pressed state to indicate switching some state within our app. To enable a single button to be clicked to toggle between the pressed and unpressed state, configure the button with enableToggle: true. Menus, Toolbars, and Buttons [ 80 ] If a set of buttons are to toggleable, but only one may be pressed at once, configure each button in that set with a toggleGroup. This is an ID string which links buttons which enforce this rule. Toggleable buttons may be configured with a toggleHandler function which is called whenever the button's state changes in either direction. { text: 'Horizontal', toggleGroup: 'orientation-selector' }, { text: 'Vertical', toggleGroup: 'orientation-selector' } This code produces the pair of buttons below in which only one orientation may be selected at once, Horizontal or Vertical: Toolbar item alignment, dividers, and spacers By default, every toolbar aligns elements to the leftmost side. There is no alignment config for a toolbar, so if we want to align all of the toolbar buttons to the rightmost side, we need to add a fill item to the toolbar. This item is sometimes referred to as a 'greedy spacer'. If we want to have items split up between both the left and right sides, we can also use a fill, but place it between items: { xtype: 'tbfill' } Pop this little guy in a toolbar wherever you want to add space and it will push items on either side of the fill to the ends of the tool bar, as shown below: We also have elements that can add space or a visual vertical divider, like the one used between the Menu Button and the Split Button. The spacer adds a few pixels of empty space that can be used to space out buttons, or move elements away from the edge of the toolbar: Chapter 4 [ 81 ] { xtype: 'tbspacer' } A divider can be added in the same way: { xtype: 'tbseparator' } Shortcuts Ext JS has many shortcuts that can be used to make coding faster. Shortcuts are a character or two that can be used in place of a configuration object. For example, consider the standard toolbar filler configuration: { xtype: 'tbfill' } The shortcut for a toolbar filler is a hyphen and a greater than symbol: '->' Not all of these shortcuts are documented. So be adventurous, poke around the source code that is distributed in the src folder of the SDK download, and see what you can find. Here is a list of the commonly-used shortcuts: Component Shortcut Description Fill '->' The fill that is used to push items to the right side of the toolbar. Separator '-' or 'separator' A vertical bar used to visually separate items. Spacer ' ' Empty space used to separate items visually. The space is two pixels wide, but can be changed by overriding the style for the xtb-spacer CSS class. TextItem 'Your Text' Add any text or HTML directly to a toolbar by simply placing it within quotes. Menus, Toolbars, and Buttons [ 82 ] Icon buttons The standard button can be used as an icon button like the ones we see used in text editors to make text bold or italic—simply a button with an icon and no text. Two steps need to be taken to make an icon button—defining an image to be used as the icon and applying the appropriate class to the button. { xtype: 'tbbutton', cls: 'x-btn-icon', icon: 'images/bomb.png' } This could just as easily be an icon beside text by changing the style class and adding the text config. { xtype: 'tbbutton', cls: 'x-btn-text-icon', icon: 'images/bomb.png', text: 'Tha Bomb' } Another method for creating an icon button is to apply a CSS class to it that contains a background image. With this method we can keep the references to images in our CSS instead of our JavaScript, which is preferable whenever possible. { xtype: 'tbbutton', iconCls: 'bomb' } The CSS to go along with this would have a background image defined, and the CSS 'important' flag set. .bomb { background-image: url( images/bomb.png ) !important; } Chapter 4 [ 83 ] Button events and handlers—click me! A button needs to do more than just look pretty—it needs to react to the user. This is where the button's handler and events come in. A handler is a function that is executed when a button is clicked. The handler config is where we add our function, which can be an anonymous function like below or a method of a class—any function will do: { xtype: 'button', text: 'Button', handler: function(){ Ext.Msg.alert('Boo', 'Here I am'); } } This code will pop up an alert message when the button is clicked. Just about every handler or event in Ext JS passes a reference to the component that triggered the event as the first argument. This makes it easy to work with whichever component that fired this handler, calling the disable method on it. { xtype: 'tbbutton', text: 'Button', handler: function(f){ f.disable(); } } We can take this reference to the button—a reference to itself—and access all of the properties and functions of that button. For this sample, we have called the disable function which grays out the button and makes it non-functional. We can have more fun than just disabling a button. Why don't we try something more useful? Loading content on menu item click Let's take our button click and do something more useful with it. For this example, we are going to add a config option named helpfile to each menu item that will be used to determine what content file to load in the body of our page: Menus, Toolbars, and Buttons [ 84 ] { xtype: 'tbsplit', text: 'Help', menu: [{ text: 'Genre', helpfile: 'genre', handler: Movies.showHelp },{ text: 'Director', helpfile: 'director', handler: Movies.showHelp },{ text: 'Title', helpfile: 'title', handler: Movies.showHelp }] } Note the helpfile config option that we have added to each of the menu items configs. We have made this config property up so that we have a way to store a variable that is unique to each menu item. This is possible because config properties can be anything we need them to be, and are copied into the Component upon construction, and become properties of the Component. In this case, we are using a config property as a reference to the name of the file we want to load. The other new thing we are doing is creating a collection of functions to handle the menu item click. These functions are all organized into a Movies object. var Movies = function() { var helpbody; return { showHelp : function(btn){ Movies.doLoad(btn.helpfile); }, doLoad : function(file){ helpbody = helpbody || Ext.getBody().createChild({tag:'div'}); helpbody.load({ url: 'html/' + file + '.txt' }); }, setQuality: function(q) { helpbody = helpbody || Ext.getBody().createChild({tag:'div'}); helpbody.update(q); Chapter 4 [ 85 ] } }; }(); This piece of code is worth reading a few times until you understand it. It illustrates the creation of a singleton object. Notice that the function has () after it. It is called immediately returning an object, instead of a class that could be instantiated. The Movies variable is assigned to reference whatever that function returns. It does not reference the function. The object returned from that function contains three functions. These are the "member functions" of the Movies singleton object. It offers utility methods which our example code will use. All the functions have access to the helpbody variable because they were declared within the same function it was declared in. They can use that variable, but it is not available to outside code. This is an important capability of singleton objects built in this slightly confusing manner. Form fields in a toolbar As the Toolbar class inherits from Container, it can be used to house any Ext JS Component. Naturally, form fields and combo boxes are very useful items to have on a toolbar. These are added as child items just as with all Container classes we have encountered so far: { xtype: 'textfield' } In the same way as we created form fields in the last chapter, we add the form fields to the items array, which will place the form fields within the toolbar. Now let's make the form field do something useful, by having it perform the same functionality as our help menu, but in a more dynamic way. { xtype: 'textfield', listeners: { specialkey: Movies.doSearch } } Menus, Toolbars, and Buttons [ 86 ] This listener is added directly to the form field's config. For this, we are using a specialkey listener, which we used in the previous chapter. This is the listener that is used to capture edit keystrokes, such as Enter and Delete among others. The handler function will be added to our small Movies class created earlier: doSearch : function(field, keyEvent){ if (keyEvent.getKey() == Ext.EventObject.ENTER) { Movies.doLoad(field.getValue()); } } Now the text field in our toolbar that enables us to type in the name of the text file to load should look like the following. Try typing in some of the samples used in our menu, such as director or title: Buttons don't have to be in a toolbar Up until now, we have been dealing with buttons in toolbars. But because buttons are part of the Ext JS Components class hierarchy, they can be used as child items of any Container, not just toolbars. When used outside of a toolbar, they are themed in a slightly different way. A button can be added to the items array, just like we would add a panel or other child components. new Ext.Window({ title: 'Help', id: 'helpwin', width: 300, height: 300, items: [{ xtype: 'button', text: 'Close', handler: function(){ Ext.getCmp('helpwin').close(); } }] }).load("html/director.txt ").show(); The above code fragment would produce the following display: Chapter 4 [ 87 ] Toolbars in panels As mentioned at the beginning of this chapter, toolbars can be configured into all Ext JS Panel classes (Panel, GridPanel, TreePanel, and Window) docked either above or below the panel, body (or both) Panel classes, such as the window and the grid, have a top and bottom toolbar config, along with a buttons config: • tbar: A toolbar, or toolbar configuration object, or an array specifying toolbar child items. This causes a toolbar to be docked above the panel's body. • bbar: A toolbar, or toolbar configuration object, or an array specifying toolbar child items. This causes a toolbar to be docked below the panel's body. • fbar: Also may be specified as buttons. A toolbar, or toolbar configuration object, or an array specifying toolbar child items, which specifies a toolbar to be placed into the panel's footer element. If we wanted to place a toolbar at the top of a window, we would specify a tbar config option which references an array of toolbar child config objects like this: new Ext.Window({ title: 'Help', width: 300, height: 300, renderTo: document.body, closeAction: 'hide', layout: 'fit', tbar: [{ text: 'Close', handler: function(){ winhelp.hide(); } Menus, Toolbars, and Buttons [ 88 ] },{ text: 'Disable', handler: function(t){ t.disable(); } }] }); When this window is activated by clicking the Director help menu entry, this produces the following display: Of course if we wanted that same toolbar on the bottom of the window, we can change from a tbar to a bbar. We can also specify a toolbar (or config of a toolbar, or toolbar's items) to be rendered into the footer area of a panel or window. Buttons are themed as normal buttons in a footer bar, and by default are aligned to the right: new Ext.Window({ title: 'Help', width: 300, height: 300, renderTo: document.body, closeAction: 'hide', layout: 'fit', fbar: [{ text: 'Close', handler: function(){ winhelp.hide(); } }] }); If the help window was configured in this way, it would display like this: Chapter 4 [ 89 ] Ext JS also has a custom toolbar for paged grids which contains all of the buttons for moving through pages of results. We will cover this special toolbar in the grid chapter later in this book. Toolbars unleashed As mentioned above, the toolbar class is a special type of Container class which lays out its child components in a particular, preconfigured way. We can use it to house more complex Components, and we can experiment with using different layout types to size and arrange the child Components in different ways. Layouts will be covered in Chapter 7, but we can see some of their power in our final toolbar example. In the final example, we use the hbox (horizontal box) layout to arrange ButtonGroups across the toolbar, and force them to be vertically sized to the size of the highest one, to produce a pleasing, even ribbon effect. new Ext.Panel({ title: 'Panel with heavyweight toolbar', renderTo: document.body, width: 600, height: 400, tbar: new Ext.Toolbar({ layout: { type: 'hbox', align: 'stretchmax' }, items: [{ xtype: 'buttongroup', title: 'Group 1', columns: 1, items: [{ Menus, Toolbars, and Buttons [ 90 ] text: 'Group 1 Button One', handler: handlerFn },{ text: 'Group 1 Button Two', handler: handlerFn },{ text: 'Group 1 Button Three', handler: handlerFn }] },{ This example will produce output like this: This example illustrates the concept that a toolbar is a Container which may contain any Component, and also that a ButtonGroup is a Container with the same heritage. The first three ButtonGroups contain Buttons; the last one for a slight change of style contains MenuItems. Summary In this chapter, we had the chance to play with a couple of different ways to create toolbar items, including using a config object or its shortcut. The many options available for toolbars make them a useful component for everything from the simplest button bar, to a complex combination of buttons, menus, and form fields. Interacting with the buttons, menus, and form fields is easy using the built-in handlers. In the next chapter we will get to know one of the most powerful and useful Components in the Ext JS Component family, The GridPanel. Displaying Data with Grids The grid is, without doubt, one of the most widely-used components of Ext JS. We all have data, and this needs to be presented to the end user in an easy-to-understand manner. The spreadsheet (a.k.a. grid) is the perfect way to do this—the concept has been around for quite a while because it works. Ext JS takes that concept and makes it flexible and downright amazing! In this chapter we will be: • Using a GridPanel to display structured data in a user-friendly manner • Reading data from the server (which provides the data from a database) to display in the grid • Working with a grid's events and manipulating the grid's features • Using some advanced data formatting and display techniques for grids • Paging data in a grid • Exploring highly efficient grid types for displaying large datasets We will cover how to define the rows and columns, but more importantly, we will learn how to make the grid a very useful part of our application. We can do this by adding custom rendered cells that contain images, and change styles based on data values. In doing this we are adding real value to our grid by breaking out of the boundaries of simple spreadsheet data display! Displaying Data with Grids [ 92 ] What is a grid anyway? Ext JS grids are similar to a spreadsheet; there are two main parts to each spreadsheet: • Columns • Rows Here our columns are Title, Released, Genre, and Price. Each of the rows contains movies such as The Big Lebowski, Super Troopers, and so on. The rows are really our data; each row in the grid represents a record of data held in a data store. A GridPanel is databound Like many Ext JS Components, such as the ComboBox we worked with in Chapter 3, the GridPanel class is bound to a data store which provides it with the data shown in the user interface. So the first step in creating our GridPanel is creating and loading a store. The data store in Ext JS gives us a consistent way of reading different data formats such as XML and JSON, and using this data in a consistent way throughout all of the Ext JS widgets. Regardless of whether this data is originally provided in JSON, XML, an array, or even a custom data type of our own, it's all accessed in the same way thanks to the consistency of the data store and how it uses a separate reader class which interprets the raw data. Chapter 5 [ 93 ] Instead of using a pre-configured class such as the ArrayStore used in our ComboBox from Chapter 3, we will explicitly define the classes used to define and load a store. The record definition We first need to define the fields which a Record contains. A Record is a single row of data and we need to define the field names, which we want to be read into our store. We define the data type for each field, and, if necessary, we define how to convert from the raw data into the field's desired data type. What we will be creating is a new class. We actually create a constructor which will be used by Ext JS to create records for the store. As the 'create' method creates a constructor, we reference the resulting function with a capitalized variable name, as per standard CamelCase syntax: var Movie = Ext.data.Record.create([ 'id', 'coverthumb', 'title', 'director', 'runtime', {name: 'released', type: 'date', dateFormat: 'Y-m-d'}, 'genre', 'tagline', {name: 'price', type: 'float'}, {name: 'available', type: 'bool'} ]); Each element in the passed array defines a field within the record. If an item is just a string, the string is used as the name of the field, and no data conversion is performed; the field's value will be whatever the Reader object (which we will learn more about soon) finds in the raw data. If data conversion is required, then a field definition in the form of an object literal instead of a string may contain the following config options: • Name: The name by which the field will be referenced. • type: The data type in which the raw data item will be converted to when stored in the record. Values may be 'int', 'float', 'string', 'date', or 'bool'. • dateFormat: If the type of data to be held in the field is a date type, then we need to specify a format string as used by the Date.parseDate function. Displaying Data with Grids [ 94 ] Defining the data type can help to alleviate future problems, instead of having to deal with all string type data defining the data type, and lets us work with actual dates, Boolean values, and numbers. The following is a list of the built in data types: Field type Description Information string String data int Number Uses JavaScript's parseInt function float Floating point number Uses JavaScript's parseFloat function boolean True/False data date Date data dateFormat config required to interpret incoming data. Now that the first step has been completed, and we have a simple Record definition in place, we can move on to the next step of configuring a Reader that is able to understand the raw data. The Reader A store may accept raw data in several different formats. Raw data rows may be in the form of a simple array of values, or an object with named properties referencing the values, or even an XML element in which the values are child nodes. We need to configure a store with a Reader object which knows how to extract data from the raw data that we are dealing with. There are three built in Reader classes in Ext JS. ArrayReader The ArrayReader class can create a record from an array of values. By default, values are read out of the raw array into a record's fields in the order that the fields were declared in the record definition we created. If fields are not declared in the same order that the values occur in the rows, the field's definition may contain a mapping config which specifies the array index from which we retrieve the field's value. JsonReader This JSONReader class is the most commonly used, and can create a record from raw JSON by decoding and reading the object's properties. Chapter 5 [ 95 ] By default, the field's name is used as the property name from which it takes the field's value. If a different property name is required, the field's definition may contain a mapping config which specifies the name of the property from which to retrieve the field's value. XmlReader An XMLReader class can create a record from an XML element by accessing child nodes of the element. By default, the field's name is used as the XPath mapping (not unlike HTML) from which to take the field's value. If a different mapping is required, the field's definition may contain a mapping config which specifies the mapping from which to retrieve the field's value. Loading our data store In our first attempt, we are going to create a grid that uses simple local array data stored in a JavaScript variable. The data we're using below in the movieData variable is taken from a very small movie database of some of my favorite movies, and is similar to the data that will be pulled from an actual server- side database later in this chapter. The data store needs two things: the data itself, and a description of the data—or what could be thought of as the fields. A reader will be used to read the data from the array, and this is where we define the fields of data contained in our array. The following code should be placed before the Ext JS OnReady function: var movieData = [ [ 1, "Office Space", "Mike Judge", 89, "1999-02-19", 1, "Work Sucks", "19.95", 1 ],[ 3, "Super Troopers", "Jay Chandrasekhar", Displaying Data with Grids [ 96 ] 100, "2002-02-15", 1, "Altered State Police", "14.95", 1 ] //...more rows of data removed for readability...// ]; var store = new Ext.data.Store({ data: movieData, , reader: new Ext.data.ArrayReader({idIndex: 0}, Movie) }); If we view this code in a browser we would not see anything—that's because a data store is just a way of loading and keeping track of our data. The web browser's memory has our data in it. Now we need to configure the grid to display our data to the user. Displaying structured data with a GridPanel Displaying data in a grid requires several Ext JS classes to cooperate: • A Store: As mentioned in Chapter 3, a data store is a client-side analogue of a database table. It encapsulates a set of records, each of which contains a defined set of fields. Full details about data stores are contained in Chapter 15. • A Record definition: This defines the fields (or "columns" in database terminology) which make up each record in the Store. Field name and datatype are defined here. More details are in Chapter 15. • A Reader which uses a Record definition to extract field values from a raw data object to create the records for a Store. • A ColumnModel which specifies the details of each column, including the column header to display, and the name of the record field to be displayed for each column. • A GridPanel: A panel subclass which provides the controller logic to bind the above classes together to generate the grid's user interface. If we were to display the data just as the store sees it now, we would end up with something like this: Chapter 5 [ 97 ] Now that is ugly—here's a breakdown of what's happening: • The Released date has been type set properly as a date, and interpreted from the string value in our data. It's provided in a native JavaScript date format—luckily Ext JS has ways to make this look pretty. • The Price column has been type set as a floating point number. Note that there is no need to specify the decimal precision. • The Avail column has been interpreted as an actual Boolean value, even if the raw data was not an actual Boolean value. As you can see, it's quite useful to specify the type of data that is being read, and apply any special options that are needed so that we don't have to deal with converting data elsewhere in our code. Before we move on to displaying the data in our grid, we should take a look at how the convert config works, as it can come in quite useful. Converting data read into the store If we need to, we can convert data as it comes into the store, massage it, remove any quirky parts, or create new fields all together. This should not be used as a way to change the display of data; that part will be handled elsewhere. A common task might be to remove possible errors in the data when we load it, making sure it's in a consistent format for later actions. This can be done using a convert function, which is defined in the 'convert' config by providing a function, or reference to a function. In this case we are going to create a new field by using the data from another field and combining it with a few standard strings. var store = new Ext.data.Store({ data: movieData, reader: new Ext.data.ArrayReader({id:'id'}, [ 'id', 'title', Displaying Data with Grids [ 98 ] 'director', {name: 'released', type: 'date', dateFormat: 'Y-m-d'}, 'genre', 'tagline', 'price', 'available', {name:'coverthumb',convert:function(v, rawData){ return 'images/'+rawData[0]+'m.jpg'; }} ]) }); This convert function when used in this manner will create a new field of data that looks like this: 'images/5m.jpg' We will use this new field of data shortly, so let's get a grid up and running. Displaying the GridPanel The class that pulls everything together is the GridPanel. This class takes care of placing the data into columns and rows, along with adding column headers, and boxing it all together in a neat little package. The movie data store we created isn't much good to anybody just sitting in the computer's memory. Let's display it in a grid by creating a simple GridPanel: 1. Let's add our data store to the following GridPanel code: Ext.onReady(function() {… var grid = new Ext.grid.GridPanel({ renderTo: Ext.getBody(), frame: true, title: 'Movie Database', height: 200, width: 520, store: store, colModel: new Ext.grid.ColumnModel({ defaultSortable: false, columns: [ {header: "Title", dataIndex: 'title'}, {header: "Director", dataIndex: 'director'}, {header: "Released", dataIndex: 'released', Chapter 5 [ 99 ] xtype: 'datecolumn'}, {header: "Genre", dataIndex: 'genre'}, {header: "Tagline", dataIndex: 'tagline'} ] }) }); 2. Bring this page up in a browser, and here's what we will see: How did that work? All except two of the config options used here should be familiar to us now because they are inherited from base classes such as the panel Believe it or not, there are only two new config options that are really essential to make a GridPanel different from a Panel! They are: • store: This references a store object which provides the data displayed in the grid. Any changes to the store are automatically applied to the UI. • ColModel: This is a ColumnModel object which defines how the column headers and data cells are to be displayed. It defines a header for each column, and the name of the field to display in that column. We can almost read through the configuration like a sentence: Render our grid into the body of the document, frame it, and give it a title of 'Movie Database'. The height will be 200 and the width 520; it will use our 'store' data store and have the columns specified. This again shows us the benefits of both object-based configuration, and the Ext JS class hierarchy. The configuration is readable, not a series of parameters whose order must be remembered. Displaying Data with Grids [ 100 ] Also, the renderTo frame, title, height, and width options are all inherited from base classes, and are common to all Panel classes. So we will never have to think about these once we have mastered the Panel class. Defining a grid's column model The ColumnModel class encapsulates a set of column objects, each of which defines an individual column's characteristics. This is a mirror image of the field definitions in the Record definition which specify how to read in a field's value from a raw data object. The ColumnModel works at the opposite end of the data flow. It defines which fields from each record to display (you don't have to show them all), and also how the value from each field is to be converted back into string form for display in the UI. The ColumnModel also maintains defaults to be applied to the columns which it manages, and offers an API to manipulate the columns, and provide information about them. To define our grid's columns, we configure the ColumnModel with an array of column config objects. Each of the objects within a ColumnModel's columns array defines one column. The most useful options within a column definition are: • header: The HTML to display in the header area at the top of the column. • dataIndex: The name of the record field—as defined in the Record definition—to display in each cell of the column. • xtype: The type of column to create. This is optional, and defaults to a basic column which displays the referenced data field unchanged. But to display a formatted date using the default date format, we can specify 'datecolumn'. There are several other column xtypes described in the API documentation. So a ColumnModel definition is like this: new Ext.grid.ColumnModel({ defaultSortable: false, columns: [ {header: 'Title', dataIndex: 'title'}, {header: 'Director', dataIndex: 'director'}, {header: 'Released', dataIndex: 'released'}, {header: 'Genre', dataIndex: 'genre'}, {header: 'Tagline', dataIndex: 'tagline'} ] }) Chapter 5 [ 101 ] This will create grid column headers that look like the following. We have also set the default of sortable for each column to false by using a master config in the Column Model: Here are some other useful config options for each column within the column model: Option Description Usage renderer Specifies a function which returns formatted HTML to display in a grid cell Can be used to format the data for this column into your preferred format. Any type of data can be transformed. We will learn about these in the next few pages. hidden Hides the column Boolean value defining whether or not the column should be displayed. hideable Allows the UI to offer checkboxes to hide/show the column If a column must not be hidden (or indeed begins hidden and must not be shown) by the user, set this option to true. width Specifies the column width in pixels The width of the column. Default is 100 pixels; overflowing content is hidden. sortable Specifies whether the column is sortable Boolean value specifying whether or not the column can be sorted. Overrides the defaultSortable configuration of the ColumnModel. Built-in column types There are several built-in column types, all identified by their own unique xtype which provide special formatting capabilities for cell data. The usage of these is illustrated in the example code for this chapter. BooleanColumn Displays the text true or false (or the locale specific equivalents if you include the correct Ext JS locale file) in the column's cells depending on whether the cell's field value is true or false. It can be configured with alternative true/false display values. Example usage: { xtype: 'booleancolumn', header: 'Available', dataIndex: 'available', trueText: 'Affirmative', falseText: 'Negative' } Displaying Data with Grids [ 102 ] DateColumn This displays the cell's field value as a formatted date. By default, it uses the date format 'm/d/Y', but it can be configured with a format option specifying an alternative format. The value in the column's associated field must be a date object. Example usage: { header: "Released", dataIndex: 'released', xtype: 'datecolumn', format: 'M d Y', width: 70 } NumberColumn Displays the cell's field value formatted according to a format string as used in Ext.util.Format.number. The default format string is "0.00". Example usage: { header: "Runtime", dataIndex: 'runtime', xtype: 'numbercolumn', format: '0', width: 70 } TemplateColumn Uses an Ext.XTemplate string to produce complex HTML with any fields from within the row's record embedded. { header: "Title", dataIndex: 'title', xtype: 'templatecolumn', tpl: ''+ '{title} '+ 'Director: {director} {tagline}' } The tokens in the template (tpl) between braces are field names from the store's record. The values are substituted in to create the rendered value. See the API documentation for the Ext.XTemplate class for more information. Chapter 5 [ 103 ] ActionColumn Displays icons in a cell, and may be configurable with a handler function to process clicks on an icon. Example illustrating arguments passed to the handler function: { header: 'Delete', sortable: false, xtype: 'actioncolumn', width: 40, align: 'center', iconCls: 'delete-movie', handler: function(grid, rowIndex, colIdex, item, e) { deleteMovie(grid.getStore().getAt(rowIndex)); } } Using cell renderers If the built in column types cannot create the desired output in a grid cell (which is very unlikely given the data formatting capabilities of the XTemplate class), then we can write a custom renderer function. We can do some pretty neat things with cell rendering. There are few limitations to stop us from making the cell look like or contain whatever we want. All that needs to be done is to specify one of the built-in cell formatting functions provided by Ext JS, such as usMoney, or we can create our own cell renderer that returns a formatted value. Let's take a look at using the built-in cell renderers first. Then we can experiment with creating our own. Formatting data using the built-in cell renderers Many built-in formatting functions exist to take care of common rendering requirements. One that I use quite often is the date renderer: renderer: Ext.util.Format.dateRenderer('m/d/Y') Some of the built-in renderers include commonly-required formatting, such as money, capitalize, and lowercase. Displaying Data with Grids [ 104 ] Here are a few of the renderers that I find most useful: Renderer Description Usage dateRenderer Formats a date for display Can be used to format the data for this column into our preferred date display format. Any type of date can be transformed. uppercase lowercase Upper and lower case conversion Converts the string to completely upper or lower case text. capitalize Pretty text Formats a text string to have correct capitalization. Creating lookup data stores—custom cell rendering We're going to start by taking the 'genre' column, which has a numeric value, and look up that value in the genre data store we created earlier in Chapter 3 to find the textual representation of our genre number. First, we add a config option to the column model that tells the columns which function to use for rendering each cell's content. {header: 'Genre', dataIndex: 'genre', renderer: genre_name} Now we need to create that function. The function being called by the column is passed the value of its cell as the first argument. The second argument is a cell object, while the third is the record for the current row being rendered, followed by row index, column index, and the store—none of which we will use for this renderer. So let's just specify the first argument as 'val' and leave off the rest. function genre_name(val) { var rec = genres.getById(val); return rec ? rec.get('genre') : val; } The renderer function is passed the value of the current cell of data. This value can be compared, massaged, and any actions we need can be performed on it— whatever value is returned by the function is rendered directly to the grid cell. A queryBy method is used to filter the data from our genre store and find the matching row of data. It accepts a function that performs a comparison against each row of data, and returns true to use the row that matches. Chapter 5 [ 105 ] Just for good measure, here is a compacted version of the same function. It's not as easy to read as the first version, but accomplishes the same result. function genre_name(val){ return genres.queryBy(function(rec){ return rec.data.id == val; }).itemAt(0).get('genre'); } Combining two columns The lookup data store is a very useful renderer. However, it's also common for developers to need to combine two columns to form a single cell. For example, to perform a calculation on a pair of columns to figure out a total, percentage, remainder, and so on, or to concatenate two or more text fields into a fancy display. Let's just take the title of our movie, and append the tagline field underneath the title. The first step will be to hide the tagline column, since it will be displayed along with the title field—we don't need it shown in two places. Hiding the column can be done in our column model, and while the column will be hidden from display, the data still exists in our store. {header: 'Tagline', dataIndex: 'tagline', hidden: true} The next step is our renderer function that will take care of combining the fields. Here we will start to use the rest of the arguments passed to the renderer function. function title_tagline(val, x, rec){ return ''+val+' '+rec.get('tagline'); } This function simply concatenates a couple of strings along with the data and returns the modified value. I went ahead and bolded the title in this sample to provide some contrast between the two pieces of data. As you can see, HTML tags work just fine within grid cells. The next step would be to add the renderer config to our column model, referencing the title_tagline function that we just created. {header: 'Title', dataIndex: 'title', renderer: title_tagline} This will make the Title column look like this: Displaying Data with Grids [ 106 ] Generating HTML and graphics Let's get some good visuals by placing an image into each row, which will show the cover art for each movie title. As we just found out, we can use plain HTML within the cell. So all that needs to happen is to create a renderer that grabs our field containing the file name of the image—we created in the store earlier—and write that into an IMG tag as the SRC attribute. function cover_image(val){ return ''; } By creating this fairly straightforward function, and setting it as the column renderer, we have an image in our grid: {header: 'Cover', dataIndex: 'coverthumb', renderer: cover_image} If you make all these renderer additions, the grid should now look like this: Built-in features Ext JS has some very nice built-in features to help complete the spreadsheet-like interface. Columns have built-in menus that provide access to sorting, displaying, and hiding columns: Chapter 5 [ 107 ] Client-side sorting Unless specified as a server-side (remotely) sorted grid, an Ext JS grid is by default able to sort columns on the client side. Server-side sorting should be used if the data is paged, or if the data is in such a format that client-side sorting is not possible. Client-side sorting is quick, easy, and built-in—just set a column's sortable config to true: {header: 'Tagline', dataIndex: 'tagline', id: 'tagline', sortable: true} We can also accomplish this after the grid has been rendered; to make this easier and predictable we need to assign an ID to the column as shown above: var colmodel = grid.getColumnModel(); colmodel.getColumnById('tagline').sortable = true; Our column model controls the display of columns and column headers. If we grab a reference to the column model by asking for it from the grid, then we can make changes to the columns after it has been rendered. We do this by using the getColumnById handler that the column model provides us with, and which accepts the column ID as the argument. Hidden/visible columns Using the column header menu, columns can be hidden or shown. This can also be changed at a config level, to have columns hidden by default, as shown below: {header: "Tagline", dataIndex: 'tagline', id: 'tagline', hidden: true} The more exciting way is to do this after the grid has been rendered, by using the functions Ext JS provides: var colmodel = grid.getColumnModel(); colmodel.setHidden(colmodel.getIndexById('tagline'),true); Grabbing a reference to the column model again will allow us to make this change. Displaying Data with Grids [ 108 ] Column reordering Dragging a column header will allow the user to reorder the entire column into a new order within the grid. All of this is enabled by default as part of the built-in functionality of the grid. Any column can be dragged to a different order in the grid. This screenshot shows the Price column being moved to between the Title and Director columns. We can disable this functionality entirely by setting a config option in the GridPanel: enableColumnMove: false This move event—and many other events in the grid—can be monitored and responded to. For example, we could monitor the movement of columns and pop up a message based on where the column was moved to: grid.getColumnModel().on('columnmoved', function(cm,oindex,nindex) { var title = 'You Moved '+cm.getColumnHeader(nindex); if (oindex > nindex){ var dirmsg = (oindex-nindex)+' Column(s) to the Left'; }else{ var dirmsg = (nindex-oindex)+' Column(s) to the Right'; } Ext.Msg.alert(title,dirmsg); } ); Many different events can be monitored using the same technique. The grid, data store, and column model each have their own set of events that can be monitored, all of which we will learn about in more detail later in this chapter. Chapter 5 [ 109 ] Displaying server-side data in the grid With Ext JS we can pull data into our web page in many ways. We started by pulling in local array data for use in the grid. Now we are going to pull the data in from an external file and a web server (database). Loading the movie database from an XML file We have this great movie database now, but each time I want to add a new movie I have to edit the JavaScript array. So why not store and pull our data from an XML file instead? This will be easier to update, and the XML file could even be generated from a database query or a custom script. Let's take a look at an example of how our XML file would be laid out: 1Office SpaceMike Judge1999-02-191Work Sucks19.9513Super TroopersJay Chandrasekhar2002-02-151Altered State Police14.951 //...more rows of data removed for readability...// The other change we would need to make is to alter the data reader, and set the location of our XML file so that the data store knows where to fetch the data from. Displaying Data with Grids [ 110 ] There are four basic changes that need to happen when moving from local to remote data: • The url option, specifying the location of our data needs to be added—this will replace the data option that we used to store local data • The reader is changed from an ArrayReader to an XmlReader to deal with the differences involved in reading from an XML format instead of an array format • The XmlReader is told which element contains a record or row of data by setting the record option • We will need to call the store's load function that tells our data store to pull in the data from the file on the server and parse it into memory var store = new Ext.data.Store({ url: 'movies.xml', reader: new Ext.data.XmlReader({ record:'row', idPath:'id' }, Movie), autoLoad: true }); Try making these changes and see if your grid still works—there should be no noticeable difference when changing data sources or formats. Note that to make the change from local to remote data and from an array format to an XML format, the only changes we need to make were to the data store. Ext JS isolates these types of changes by using a common data store that is able to use an external reader to read many formats and store them internally in the same way. Loading the movie database from a JSON file We're in the same boat as XML with this data format. Just changing the reader and setting up some config options will take care of everything. The JSON rows of data are expected to be in the form of an array of objects—our movies.json file will therefore contain data that looks like this: { success:true, rows:[ { "id":"1", "title":"Office Space", Chapter 5 [ 111 ] "director":"Mike Judge", "released":"1999-02-19", "genre":"1", "tagline":"Work Sucks", "price":"19.95", "active":"1" },{ "id":"3", "title":"Super Troopers", "director":"Jay Chandrasekhar", "released":"2002-02-15", "genre":"1", "tagline":"Altered State Police", "price":"14.95", "active":"1" } //...more rows of data removed for readability...// ] } The main difference between setting up a JSON reader versus an XML reader, is that the JSON reader needs to know the name of the root element that holds our array of objects (the rows of data). So instead of specifying a record config, we need to specify a root config: var store = new Ext.data.Store({ url: 'movies.json', reader: new Ext.data.JsonReader({ root:'rows', idProperty:'id' }, Movie), autoLoad: true }); This grid will have an identical look and the same functionality as the array and the XML grids that we created earlier. JSON and arrays are a format native to JavaScript called an Object Literal and Array Literal, and will end up being the quickest formats for the data store (JavaScript) to read, which means that our grid will be displayed much faster than with most other formats, specifically XML. Displaying Data with Grids [ 112 ] Loading data from a database using PHP The setup for our GridPanel stays the same. But instead of grabbing a static file with the JSON data, we can pull the data from a PHP script that will fetch the data from a database, and format it into JSON that Ext JS is able to read: The PHP code used in these examples is meant to be the bare minimum needed to get the job done. In a production environment you would want to account for security against SQL injection attacks, other error checking, and probably user authentication—which the example code does not account for. Programming the grid Most of the code we have written so far concerns configuring the grid to be displayed. Often, we will want the grid to do something in response to user input—interaction. One of the common interactions in a grid is to select or move the rows of data. Ext JS refers to this interaction and how it's handled as the "selection model". Let's see how to set up a selection model. Working with cell and row selections Ext JS grids delegate the monitoring of user interaction with the grids rows, cells, and columns to a separate selection model object. The selection model is used to determine how rows, columns, or cells are selected, and how many items can be selected at a time. This allows us to create listeners for these selection events, along with giving us a way to query which rows have been selected. Chapter 5 [ 113 ] The built-in selection models are: • CellSelectionModel: This lets the user to select a single cell from the grid • RowSelectionModel: This lets the user select an entire row, or multiple rows from the grid • CheckBoxSelectionModel: This one uses a checkbox to enable row selection Choosing a selection model is something that depends on your project's requirements. For our movie database, we will use a row selection model, which is the most commonly used type of selection model, and just happens to be the default. The selection model is defined in the GridPanel config by using the selModel config option—the shortform sm could also be used. selModel: new Ext.grid.RowSelectionModel({ singleSelect: true }) We will also pass the selection model a config that specifies single row selections only. This stops the user from selecting multiple rows at the same time. Listening to our selection model for selections Listeners for a grid can be included in many different places depending on the desired interaction. Earlier, we applied a listener to our column model because we wanted to listen for column activity. Here, we will add a listener to the selection model because we want to know when a user has selected a movie. sm: new Ext.grid.RowSelectionModel({ singleSelect: true, listeners: { rowselect: function(sm, index, record) { Ext.Msg.alert('You Selected',record.get('title')); } } }) Displaying Data with Grids [ 114 ] The above listener code will result in the following display: Selecting a row now brings up an alert dialog. Let's take a look at what is happening here: • A listener is set for the rowselect event. This waits for a row to be selected, and then executes our function when this happens • Our function is passed a selection model, the numeric index of the row selected (starting with zero for the first row), and the data record of the row that was selected • Using the data record that our function received, we can grab the title of the movie selected and put it into a message dialog Manipulating the grid (and its data) with code There are many ways to programmatically manipulate the grid and its data. The key is to understand how the responsibility for managing the grid is broken down between the objects that we use to put a grid together. In the following discussions we will show use of the store and its associated records for manipulating the data, and use of the selection model for determining how the user is interacting with the grid. Chapter 5 [ 115 ] Altering the grid at the click of a button Here, we are going to add a top toolbar, which will have a button that brings up a prompt allowing the movie title to be edited. This will use a toolbar and buttons which we explored in Chapter 4, along with the MessageBox from Chapter 2. tbar: [{ text: 'Change Title', handler: function(){ var sm = grid.getSelectionModel(), sel = sm.getSelected(); if (sm.hasSelection()){ Ext.Msg.show({ title: 'Change Title', prompt: true, buttons: Ext.MessageBox.OKCANCEL, value: sel.get('title'), fn: function(btn,text){ if (btn == 'ok'){ sel.set('title', text); } } }); } } }] The result of this addition is as follows: Displaying Data with Grids [ 116 ] All we are really doing here is changing the data in the data store, which updates the grid automatically. The data in our database on the web server has stayed the same, and the web server has no idea whether anything has changed. It's up to us to communicate this change to the server via an AJAX request or via some other method we may prefer to use. This is covered in the next chapter in case you are wondering. Let's take a quick look at what's happening here: • sm: The selection model is retrieved from our grid • sel: We used the selection model to retrieve the row that has been selected • sel.get: Using the get method of the currently selected record, we can grab a field's value • sel.set: Using the set method of the currently selected record, we can change a field's value This basic method can be used to create many fun user interactions. Our limitation is that there are only 24 hours in a day, and sleep catches up with everyone! Advanced grid formatting As we are in the mood to create some user-grid interactions, let us add some more buttons that do fun stuff. We will now add a button to the top toolbar to allow us to hide or show a column. We will also change the text of the button based on the visibility of the column—a fairly typical interaction: { text: 'Hide Price', handler: function(btn){ var cm = grid.getColumnModel(), pi = cm.getIndexById('price'); // is this column visible? if (cm.isHidden(pi)){ cm.setHidden(pi,false); btn.setText('Hide Price'); }else{ cm.setHidden(pi,true); btn.setText('Show Price'); } } } Chapter 5 [ 117 ] We have used a new method here—getIndexById, which, as you can imagine, gets the column index, which will be a number from zero to one less than the total number of columns. This number is an indicator of where that column is in relation to the other columns. In our grid code, the column price is the fourth column, which means that the index is three because indexes start at zero. Paging the grid Paging requires that we have a server-side element (script) which will break up our data into pages. Let's start with that. PHP is well-suited to this, and the code is easy to understand and interpret into other languages. So we will use PHP for our example. When a paging grid is paged, it will pass start and limit parameters to the server-side script. This is typical of what's used with a database to select a subset of records. Our script can read in these parameters and use them pretty much verbatim in the database query. The start value represents which row of data to start returning, and the limit specifies how many total rows of data to return from the starting point. Here is a typical PHP script that would handle paging. We will name the file movies-paging.php. Displaying Data with Grids [ 118 ] This PHP script will take care of the server-side part of paging. So now we just need to add a paging toolbar to the grid—it's really quite simple! Earlier we had used a top toolbar to hold some buttons for messing with the grid. Now we are going to place a paging toolbar in the bottom toolbar slot (mostly because I think paging bars look dumb on the top). The following code will add a paging toolbar: bbar: new Ext.PagingToolbar({ pageSize: 3, store: store }) And of course we need to change the url of our data store to the url of the PHP server-side paging code. A totalProperty is also required when paging data. This is the variable name that holds the total record count of rows in the database as returned from the server side script. This lets the paging toolbar figure out when to enable and disable the previous and next buttons among other things. var store = new Ext.data.Store({ url: 'movies-paged.php', reader: new Ext.data.JsonReader({ root:'rows', totalProperty: 'results', idProperty:'id' }, Movie) }); Instead of autoLoading the store, we kick off the Store's loading process by asking the PagingToolbar to load the first page because it knows what parameters to send, so we do not need to pass any in a programmatic call of the store's load method. grid.getBottomToolbar().changePage(1); Chapter 5 [ 119 ] The result will look like this: Grouping Grouping grids are used to provide a visual indication that sets of rows are similar to each other, such as being in the same movie genre. It also provides us with sorting that is confined to each group. So if we were to sort by the price column, the price would sort only within each group of items. Grouping store Grouping of data by common values of one field is provided by a special data store class called GroupingStore. The setup is similar to a standard store. We just need to provide a few more configuration options, such as the sortInfo and the groupField. No changes to the actual data are needed because Ext JS takes care of grouping on the client side. var store = new Ext.data.GroupingStore({ url: 'movies.json', sortInfo: { field: 'genre', direction: "ASC" }, groupField: 'genre', reader: new Ext.data.JsonReader({ root:'rows', idProperty:'id' }, Movie) }); Displaying Data with Grids [ 120 ] We also need to add a view configuration to the grid panel. This view helps the grid to visually account for grouped data. var grid = new Ext.grid.GridPanel({ renderTo: document.body, frame:true, title: 'Movie Database', height:400, width:520, store: store, autoExpandColumn: 'title', colModel: // column model goes here //, view: new Ext.grid.GroupingView({ forceFit: true, groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})' }) }); After making the changes needed for a grouping grid, we end up with something that looks like this: If you now expand the context menu for the column headings, you will see a new item in the menu labeled Group By This Field that will allow the user to change the grouping column on the fly. Chapter 5 [ 121 ] Summary We have learned a lot in this chapter about presenting data in a grid. With this new-found knowledge we will be able to organize massive amounts of data into easy-to-understand grids. Specifically, we covered: • Creating data stores and grids for display • Reading XML and JSON data from a server and displaying it in a grid • Rendering cells of data for a well formatted display • Altering the grid based on user interaction We also discussed the intricacies of each of these elements, such as reading data locally or from a server—along with paging and formatting cells using HTML, images, and even lookups into separate data stores. Now that we've learned about standard grids, we're ready to take it to the next level, by making our grid cells editable just like a spreadsheet—which is the topic of the next chapter. Editor Grids In the previous chapter we learned how to display data in a structured grid that users could manipulate. But one major limitation was that there was no way for the users to edit the data in the grid in-line. Fortunately, Ext JS provides an EditorGridPanel, which allows the use of form field type editing in-line—which we will learn about it in this chapter. This works much like Excel or other spreadsheet programs, allowing the user to click on and edit cell data in-line with the grid. In this chapter we will learn to: • Present the user with editable grids that are connected to a data store • Send edited data back to the server, enabling users to update server-side databases using the Ext JS editor grid • Manipulate the grid from program code, and respond to events • Use tricks for advanced formatting and creating more powerful editing grids But first, let's see what we can do with an editable grid. Editor Grids [ 124 ] What can I do with an editable grid? The EditorGridPanel is very similar to the forms we were working with earlier. In fact, an editor grid uses the exact same form fields as our form. By using form fields to perform the grid cell editing we get to take advantage of the same functionality that a form field provides. This includes restricting input, and validating values. Combine this with the power of an Ext JS GridPanel, and we are left with a widget that can do pretty much whatever we want. All of the fields in this table can be edited in-line using form fields such as the text field, date field, and combo box. Working with editable grids The change from a non-editable grid to an editable grid is quite a simple process to start with. The complexity comes into the picture when we start to create a process to handle edits and send that data back to the server. Once we learn how to do it, that part can be quite simple as well. Let's see how we would update the grid we created at the start of Chapter 5 to make the title, director, and tagline editable. Here's what the modified code will look like: var title_edit = new Ext.form.TextField(); var director_edit = new Ext.form.TextField({vtype: 'name'}); var tagline_edit = new Ext.form.TextField({ maxLength: 45 }); var grid = new Ext.grid.EditorGridPanel({ renderTo: document.body, frame:true, title: 'Movie Database', height:200, width:520, clickstoEdit: 1, Chapter 6 [ 125 ] store: store, columns: [ {header: "Title", dataIndex: 'title', editor: title_edit}, {header: "Director", dataIndex: 'director', editor: director_edit}, {header: "Released", dataIndex: 'released', renderer: Ext.util.Format.dateRenderer('m/d/Y')}, {header: "Genre", dataIndex: 'genre', renderer: genre_name}, {header: "Tagline", dataIndex: 'tagline', editor: tagline_edit} ] }); There are four main things that we need to do to make our grid editable. These are: • The grid definition changes from being Ext.grid.GridPanel to Ext.grid.EditorGridPanel • We add the clicksToEdit option to the grid config—this option is not required, but defaults to two clicks, which we will change to one click • Create a form field for each column that we would like to be editable • Pass the form fields into our column model via the editor config The editor can be any of the form field types that already exist in Ext JS, or a custom one of our own. We start by creating a text form field that will be used when editing the movie title. var title_edit = new Ext.form.TextField(); Then add this form field to the column model as the editor: {header: "Title", dataIndex: 'title', editor: title_edit} The next step will be to change from using the GripPanel component to using the EditorGridPanel component, and to add the clicksToEdit config: var grid = new Ext.grid.EditorGridPanel({ renderTo: document.body, frame: true, title: 'Movie Database', height: 200, width: 520, clickstoEdit: 1, // removed extra code for clarity }) Editor Grids [ 126 ] Making these changes has turned our static grid into an editable grid. We can click on any of the fields that we set up editors for, and edit their values, though nothing really happens yet. Here we see that some changes have been made to the titles of a few of the movies, turning them into musicals. The editor gets activated with a single click on the cell of data; pressing Enter, the Tab key, or clicking away from the field will record the change, and pressing the Escape key will discard any changes. This works just like a form field, because, well… it is a form field. The little red tick that appears in the upper-left corner indicates that the cell is 'dirty', which we will cover in just a moment. First, let's make some more complex editable cells. Editing more cells of data For our basic editor grid, we started by making a single column editable. To set up the editor, we created a reference to the form field: var title_edit = new Ext.form.TextField(); Then we used that form field as the editor for the column: {header: "Title", dataIndex: 'title', editor: title_edit} Those are the basic requirements for each field. Now let's expand upon this knowledge. Edit more field types Now we are going to create editors for the other fields. Different data types have different editor fields and can have options specific to that field's needs. Any form field type can be used as an editor. These are some of the standard types: • TextField • NumberField Chapter 6 [ 127 ] • ComboBox • DateField • TimeField • CheckBox These editors can be extended to achieve special types of editing if needed, but for now, let's start with editing the other fields we have in our grid—the release date and the genre. Editing a date value A DateField will work perfectly for editing the release date column in our grid. So let's use that. We first need to set up the editor field and specify which format to use: release_edit = new Ext.form.DateField({ format: 'm/d/Y' }); Then we apply that editor to the column, along with the renderer that we used earlier: {header: "Released", dataIndex: 'released', renderer: Ext.util.Format.dateRenderer('m/d/Y'), editor: release_edit} This column also takes advantage of a renderer, which will co-exist with the editor. Once the editor field is activated with a single click, the renderer passes the rendering of the field to the editor and vice versa. So when we are done editing the field, the renderer will take over formatting the field again. Editor Grids [ 128 ] Editing with a ComboBox Let's set up an editor for the genres column that will provide us with a list of the valid genres to select from—sounds like a perfect scenario for a combo box. var genre_edit = new Ext.form.ComboBox({ typeAhead: true, triggerAction: 'all', mode: 'local', store: genres, displayField: 'genre', valueField: 'id' }); Simply add this editor to the column model, like we did with the others: {header: "Genre", dataIndex: 'genre', renderer: genre_name, editor: genre_edit} Now we end up with an editable field that has a fixed selection of options. Reacting to a cell edit Of course, we now need to figure out how to save all of this editing that we have been doing. I am sure the end user would not be so happy if we threw away all of their changes. We can start the process of saving the changes by listening for particular edit events, and then reacting to those with our own custom handler. Before we start coding this, we need to understand a bit more about how the editor grid works. What's a dirty cell? A field that has been edited and has had its value changed is considered to be 'dirty' until the data store is told otherwise. Records within the data store which have been created, modified, or deleted are tracked, and maintained in a list of uncommitted changes until the store is told that the database has been synchronized with these changes. Chapter 6 [ 129 ] We can tell the store to clear the dirty status of a record by calling the commit method. This signifies that we have synchronized the database, and the record can now be considered 'clean'. Let's imagine e as an edit event object. We could restore the edited record to its unmodified, 'clean' state by calling the reject method: e.record.reject(); Alternatively, we can tell the store that the database has been synchronized, and that the changes can be made permanent: e.record.commit(); Reacting when an edit occurs To save our users changes to the data store, we are going to listen for an edit event being completed, which is accomplished by listening for the afteredit event. The listener we need is added to the grid panel: var grid = new Ext.grid.EditorGridPanel({ // more config options clipped //, title: 'Movie Database', store: store, columns: // column model clipped //, listeners: { afteredit: function(e){ if (e.field == 'director' && e.value == 'Mel Gibson'){ Ext.Msg.alert('Error','Mel Gibson movies not allowed'); e.record.reject(); }else{ e.record.commit(); } } } }); Editor Grids [ 130 ] As with other events in Ext JS, the editor grid listeners are given a function to execute when the event occurs. The function for afteredit is called with a single argument: an edit object, which has a number of useful properties. We can use these properties to make a decision about the edit that just happened. Property Description grid The grid that the edit event happened in record The entire record that's being edited; other column values can be retrieved using this objects get method field The name of the column that was edited value A string containing the new value of the cell originalValue A string containing the original value of the cell row The index of the row that was edited column The index of the column that was edited For instance, if we wanted to make sure that movies directed by Mel Gibson never made it into our database, we could put a simple check in place for that scenario: if (e.field == 'director' && e.value == 'Mel Gibson'){ Ext.Msg.alert('Error','Mel Gibson movies not allowed'); e.record.reject(); }else{ e.record.commit(); } First, we check to see that the director field is the one being edited. Next, we make sure the new value entered for this field is not equal to Mel Gibson. If either of these is false, we commit the record back to the data store. This means that once we call the commit method, our primary data store is updated with the new value. e.record.commit(); We also have the ability to reject the change—sending the changed value into the black hole of space, lost forever. e.record.reject(); Of course, all we have done so far is update the data that is stored in the browser's memory. I'm sure you're just dying to be able to update a web server. We will get to that soon enough. Deleting and adding in the data store We are going to create two buttons to allow us to alter the data store—to add or remove rows of data. Let's set up a top toolbar (tbar) in the grid to contain these buttons: Chapter 6 [ 131 ] var grid = new Ext.grid.EditorGridPanel({ // more config options clipped //, tbar: [{ text: 'Remove Movie' }] } Removing grid rows from the data store Let's expand on the remove button that we just added to the toolbar in our grid. When this button is clicked, it will prompt the user with a dialog that displays the movie title. If the Yes button is clicked, then we can remove the selected row from the data store, otherwise we will do nothing. { text: 'Remove Movie', icon: 'images/table_delete.png', cls: 'x-btn-text-icon', handler: function() { var sm = grid.getSelectionModel(), sel = sm.getSelected(); if (sm.hasSelection()){ Ext.Msg.show({ title: 'Remove Movie', buttons: Ext.MessageBox.YESNOCANCEL, msg: 'Remove ' + sel.data.title + '?', fn: function(btn){ if (btn == 'yes'){ grid.getStore().remove(sel); } } }); }; } } Editor Grids [ 132 ] Let's take a look at what is happening here. We have defined some variables that we will use to determine if there were selections made, and what the selections were: • sm: The selection model is retrieved from our grid • sel: We used the selection model to retrieve the row that has been selected • grid.getStore().remove(sel): Passing the data store remove function will remove that record from the store and update the grid It's as simple as that. The local data store that resides in the browser's memory has been updated. But what good is deleting if you can't add anything—just be patient, grasshopper! Adding a row to the grid To add a new row, we use the record constructor that we create to represent a movie object, and instantiate a new record. We used the Ext.data.Record.create function to define a record constructor called Movie, so to insert a new movie into the store is as simple as this: { text: 'Add Movie', icon: 'images/table_add.png', cls: 'x-btn-text-icon', handler: function() { grid.getStore().insert(0, new Movie({ title: 'New Movie', director: '', genre: 0, tagline: '' }) ); grid.startEditing(0,0); } } The first argument to the insert function is the point at which the record inserted. I have chosen zero, so the record will be inserted at the very top. If we wanted to insert the row at the end we could simply retrieve the row count for our data store. As the row index starts at zero and the count at one, incrementing the count is not necessary because the row count will always be one greater than the index of the last item in the store. grid.getStore().insert( grid.getStore().getCount(), new Movie({ Chapter 6 [ 133 ] title: 'New Movie', director: '', genre: 0, tagline: '' }) ); grid.startEditing(grid.getStore().getCount()-1,0); Now let's take a closer look at inserting that record. The second argument is the new record definition, which can be passed with the initial field values. new Movie({ title:'New Movie', director:'', genre:0, tagline:'' }) After inserting the new row, we call the startEditing method that will activate a cell's editor. This function just needs a row and column index number to activate the editor for that cell. grid.startEditing(0,0); This gives our user the ability to start typing the movie title directly after clicking the Add Movie button. Quite a nice user interface interaction which our end users will no doubt love. Editor Grids [ 134 ] Saving edited data to the server Everything we have done so far is related to updating the local data store residing in the memory of the web browser. More often than not, we will want to save our data back to the server to update a database, file system, or something along those lines. This section will cover some of the more common requirements of grids used in web applications to update server-side information. • Updating a record • Creating a new record • Deleting a record Sending updates back to the server Earlier, we set up a listener for the afteredit event. We will be using this afteredit event to send changes back to the server on a cell-by-cell basis. To update the database with cell-by-cell changes, we need to know three things: • field: What field has changed • Value: What the new value of the field is • record.id: Which row from the database the field belongs to This gives us enough information to be able to make a distinct update to a database. We communicate with the server (using Ajax) by calling the connection request method. listeners: { afteredit: function(e){ Ext.Ajax.request({ url: 'movie-update.php', params: { action: 'update', id: e.record.id, field: e.field, value: e.value }, success: function(resp,opt) { e.record.commit(); }, failure: function(resp,opt) { e.record.reject(); } Chapter 6 [ 135 ] }); } } This will send a request to the movie-update.php script with four parameters in the form of post headers. The params we will pass into the request methods config as an object, which are all sent through the headers to our script on the server side. The movie-update.php script should be coded to recognize the 'update' action and then read in the id, field, and value data and then proceed to update the file system or database, or whatever else we need to do to make the update happen on the server side. This is what's available to us when using the afteredit event: Option Description grid Reference to the current grid record Object with data from the row being edited field Name of the field being edited value New value entered into the field originalValue Original value of the field row Index of the row being edited—this will help in finding it again column Index of the column being edited Deleting data from the server When we want to delete data from the server, we can handle it in very much the same way as an update—by making a call to a script on the server, and telling it what we want to be done. For the delete trigger, we will use another button in the grids toolbar, along with a confirm dialog to ask the user if they are sure they want to delete the record before actually taking any action. { text: 'Remove Movie', icon: 'images/table_delete.png', cls: 'x-btn-text-icon', handler: function() { var sm = grid.getSelectionModel(), sel = sm.getSelected(); if (sm.hasSelection()){ Ext.Msg.show({ Editor Grids [ 136 ] title: 'Remove Movie', buttons: Ext.MessageBox.YESNOCANCEL, msg: 'Remove '+sel.data.title+'?', fn: function(btn){ if (btn == 'yes'){ Ext.Ajax.request({ url: 'movie-update.php', params: { action: 'delete', id: e.record.id }, success: function(resp,opt) { grid.getStore().remove(sel); }, failure: function(resp,opt) { Ext.Msg.alert('Error', 'Unable to delete movie'); } }); } } }); }; } } Just as with edit, we are going to make a request to the server to have the row deleted. The movie-update.php script would see that the action is delete and the record id that we passed; it will then execute the appropriate action to delete the record on the server side. Saving new rows to the server Now we're going to add another button that will add a new record. It sends a request to the server with the appropriate parameters and reads the response to figure out what the insert id from the database was. Using this insert id, we are able to add the record to our data store with the unique identifier generated on the server side for that record. { text: 'Add Movie', icon: 'images/table_add.png', cls: 'x-btn-text-icon', handler: function() { Chapter 6 [ 137 ] Ext.Ajax.request({ url: 'movies-update.php', params: { action: 'create', title: 'New Movie' }, success: function(resp,opt) { var insert_id = Ext.util.JSON.decode( resp.responseText ).insert_id; grid.getStore().insert(0, new Movie({ id: insert_id, title: 'New Movie', director: '', genre: 0, tagline: '' }, insert_id) ); grid.startEditing(0,0); }, failure: function(resp,opt) { Ext.Msg.alert('Error','Unable to add movie'); } }); } } Much like editing and deleting, we are going to send a request to the server to have a new record inserted. This time, we are actually going to take a look at the response to retrieve the insert id (the unique identifier for that record) to pass to the record constructor, so that when we start editing that record, it will be easy to save our changes back to the server. success: function(resp,opt) { var insert_id = Ext.util.JSON.decode( resp.responseText ).insert_id; grid.getStore().insert(0, new Movie({ id: insert_id, title: 'New Movie', director: '', genre: 0, tagline: '' Editor Grids [ 138 ] }, insert_id) ); grid.startEditing(0,0); } Our success handler accepts a couple of arguments; the first is the response object, which has a property that contains the response text from our movie-update.php script. As that response is in a JSON format, we're going to decode it into a usable JavaScript object and grab the insert id value. var insert_id = Ext.util.JSON.decode( resp.responseText ).insert_id; When we insert this row into our data store, we can use this insert id that was retrieved. RowEditor plugin One of the more popular ways to edit data in a grid uses a User Extension called the RowEditor. This extension presents the user with all the editable fields at once, along with save and cancel buttons. This allows editing of all fields within a record before committing all changes at once. Since this is a plugin, we simply need to include the additional JavaScript and CSS files and configure our Grid to use the plugin. Using this plugin requires that we include the RowEditor JavaScript file, which can be found in the examples/ux folder of the Ext JS SDK download. Be sure to include it directly after the ext-all.js file. We also need to include the RowEditor styles found in the ux/css folder, (the file is called RowEditor.css,) along with the two images needed that are found in the ux/images folder, called row-editor-bg.gif and row-editor-btns.gif. Now we just need to configure the EditorGrid to use this plugin. var grid = new Ext.grid.EditorGridPanel({ // more config options clipped //, title: 'Movie Database', store: store, columns: // column model clipped //, plugins: [new Ext.ux.grid.RowEditor()] }); Chapter 6 [ 139 ] From here we have the plugin in place, but it doesn't really do much as far as saving the changes. So let's throw a writable data store in the mix to make this editor plugin much more powerful. Writable store In our final example of this chapter we will combine use of the RowEditor with a writable data store to implement full CRUD functionality for movie database maintenance. First, let's examine how to set up a writable data store. An Ext JS 3.0+ data store understands four operations to perform which require synchronization with a server: • Create: A record has been inserted into the data store • Read: Data store needs to be filled from the server • Update: A record has been modified within the data store • Destroy: A record has been removed from the data store To enable the data store to perform this synchronization, we configure it with an API object which specifies a URL to use for each operation. In our case we use the same PHP script passing a parameter specifying the action to perform: api: { create : 'movies-sync.php?action=create', read : 'movies-sync.php?action=read', update: 'movies-sync.php?action=update', destroy: 'movies-sync.php?action=destroy' }, This config option takes the place of the url config which we specified when the store was a simple load-only store. We also need to configure a writer which will serialize the changes back into the format that the reader uses. In our case, we were using a JsonReader, so we use a JsonWriter. We tell the JsonWriter to submit the full record field set upon update, not only the modified fields because the PHP script creates an SQL statement which sets all of the row's columns: writer: new Ext.data.JsonWriter({ writeAllFields: true }), Editor Grids [ 140 ] We configure the data store to not automatically synchronize itself with the server whenever its data changes, as otherwise it would synchronize during row editing, and we want to submit our changes only when we have edited the whole row: autoSave: false, We must not forget to handle exceptions that may happen with the complex operations being performed by the data store. A data store may fire an exception event if it encounters an error, and handling these errors is highly recommended. It also relays exception events from the communication layer (The Proxy object we mentioned in the last chapter). These will pass a type parameter which specifies whether it was the client or the server which produced the error: listeners: { exception: function(proxy, type, action, o, result, records) { if (type = 'remote') { Ext.Msg.alert("Could not " + action, result.raw.message); } else if (type = 'response') { Ext.Msg.alert("Could not " + action, "Server's response could not be decoded"); } else { Ext.Msg.alert("Store sync failed", "Unknown error"); } } } You can test this error handling by triggering a "deliberate mistake" in the PHP update script. If you submit one of the textual fields containing an apostrophe, the creation of the SQL statement will be incorrect because it concatenates the statement placing the values within apostrophes. The example will display an alert box informing the user of a server side error in an orderly manner. To configure the UI side of this, all we need to do is create a RowEditor, and specify it as a plugin of the grid: var rowEditor = new Ext.ux.grid.RowEditor({ saveText: 'Update', listeners: { afteredit: syncStore } }); var grid = new Ext.grid.GridPanel({ renderTo: document.body, plugins: rowEditor, ... Chapter 6 [ 141 ] The RowEditor uses the editors configured into the ColumnModel in the same way as our previous examples did. All we need to do is specify a listener for the afteredit event which the RowEditor fires when the user clicks the Update button after a successful edit. The function to make the store synchronize itself with the server is very simple: function syncStore(rowEditor, changes, r, rowIndex) { store.save(); } Running the final example in the chapter, and double-clicking to edit a row will look like this: Editor Grids [ 142 ] Summary The Ext JS Editor Grid functionality is one of the most advanced portions of the framework. With the backing of the Ext data package, the grid can pull information from a remote server in an integrated manner—this support is built into the grid class. Thanks to the numerous configuration options available, we can present this data easily, and in a variety of forms, and set it up for manipulation by our users. In this chapter, we've seen how the data support provided by the grid offers an approach to data manipulating that will be familiar to many developers. The amend and commit approach allows fine-grained control over the data that is sent to the server when used with a validation policy, along with the ability to reject changes. As well as amending the starting data, we've seen how the grid provides functionality to add and remove rows of data. We've also shown how standard Ext JS form fields such as the ComboBox can be integrated to provide a user interface on top of this functionality. With such strong support for data entry, the grid package provides a very powerful tool for application builders. For the first time in this book we have utilized a User Extension to create additional functionality that did not previously exist in the Ext JS library. This functionality can be easily shared and updated independent of the Ext JS library, which opens up a huge world of possibilities. In the next chapter, we'll demonstrate how components such as the grid can be integrated with other parts of an application screen by using the extensive layout functionality provided by the Ext JS framework. Layouts One of those foundation classes that we have already seen in action is the Container class. We have used panels, and panels are Containers (because they inherit from that base class). They can contain child components. These child components may of course be Containers themselves. A Container delegates the responsibility for rendering its child components into its DOM structure to a layout manager. The layout manager renders child components, and may, if configured to do so, perform positioning and sizing on those child components. This is one of the most powerful concepts within the Ext JS library, and it is what turns a collection of forms, grids, and other widgets, into a dynamic, fluid web application which behaves like a desktop application. It takes some time to understand this concept, so let's examine it. What is a layout manager? A layout manager is an object which renders child components for a Container. The default layout manager simply renders child components serially into the Container's DOM, and then takes no further responsibility. No sizing is performed by the default layout manager and so if the Container ever changes size, the child components will not be resized. Other built-in layout managers, depending on their type and configuration, apply sizing and positioning rules to the child components. A layout manager may also examine hints configured into the child components to decide how the child component is to be sized and positioned. The form and border layout are examples of this. Layouts [ 144 ] To see these principles in action, let's take a look at our first example that will use an hbox layout in our panel to arrange two child BoxComponents. The requirement is that the two child boxes are arranged horizontally, with a 50/50 width allocation, and they must fill the container height. The code which creates the panel is: new Ext.Panel({ renderTo: document.body, title: "I arranged two boxes horizontally using 'hbox' layout!", height: 400, width: 600, layout: { type: 'hbox', align: 'stretch', padding: 5 }, items: [{ xtype: 'box', flex: 1, style: 'border: 1px solid #8DB2E3', margins: '0 3 0 0', html: 'Left box' }, { xtype: 'box', flex: 1, style: 'border: 1px solid #8DB2E3', margins: '0 0 0 2', html: 'Right box' }], style: 'padding:10px' }); The layout configuration is an object which specifies not only which layout class is to be used, but also acts as a config object for that class. In this case we use layout type hbox, which is a layout that arranges child components as a row of horizontal boxes. The configuration options are all documented in the API docs, but two that we use here are: • padding: Specifies how much internal padding to leave within the Container's structure when sizing and positioning children. We want them to be inset so that we can visually appreciate the structure. • align: Specifies how to arrange the height of the children. 'stretch' means that they will be stretched to take up all available height. Chapter 7 [ 145 ] The hbox layout manager reads hint configurations from the child components when arranging them. Two of the configuration options that we use here are: • flex: A number which specifies the ratio of the total of all flex values to use to allocate available width. Both children have a value of 1, so the space allocated to each will be 1/2 of the available width. • margins: The top, right, bottom, and left margin width in pixels. We want to see a neat five pixel separation between the two child boxes. The result of running this example is: This simple panel is not resizable as we have constructed it. As we are using a layout which applies rules to how child items are arranged, if the Panel were to change size, both child items would have their sizes and positions recalculated according to the configuration. This is the power of the layout system which is an integral part of Container/ Component nesting: dynamic sizing. So what layouts are available? While designing your UI, you must first plan how you require any child components within it to be arranged and sized. With this in mind, you must then plan which layouts to use to achieve that goal. This requires an appreciation of what is available. The layout types built into Ext JS will now be discussed. AbsoluteLayout Type: 'absolute'. This layout allows you to position child components at X and Y coordinates within the Container. This layout is rarely used. AccordionLayout Type: 'acordion'. This layout may only use panels as child items. It arranges the panels vertically within the Container, allowing only one of the panels to be expanded at any time. The expanded panel takes up all available vertical space, leaving just the other panel headers visible. Layouts [ 146 ] Note that this layout does not by default set the child panels' widths, but allows them to size themselves. To force it to size the child panel's widths to fit exactly within the Container, use autoWidth: false in the layout config object. AnchorLayout Type: 'anchor'. This layout manager reads hints from the child components to anchor the widths and heights of the child components to the right and bottom Container borders. This layout reads an optional anchor hint from the child items. This is a string containing the width anchor and the height anchor. This layout can be quite useful for form items. BorderLayout Type: 'border'. This layout manager docks child components to the north, south, east, or west borders of the Container. Child components must use a region config to declare which border they are docked to. There must always be a region: 'center' child component which uses the central space between all docked children. North and south child components may be configured with an initial height. East and west child components may be configured with an initial width. CardLayout Type: 'card'. This layout manager arranges child components in a stack like a deck of cards, one below the other. The child components are sized to fit precisely within the Container's bounds with no overflow. Only one child component may be visible (termed active) at once. The TabPanel class uses a CardLayout manager internally to activate one tab at a time. ColumnLayout Type: 'column'. The ColumnLayout manager arranges child components floating from left to right across the Container. Hints may be used in the child components to specify the proportion of horizontal space to allocate to a child. This layout allows child components to wrap onto the next line when they overflow the available width. FitLayout Type: 'fit'. This layout manager manages only one child component. It handles sizing of the single child component to fit exactly into the Container's bounds with no overflow. This layout is often used in conjunction with a Window Component. Chapter 7 [ 147 ] FormLayout Type: 'form'. This is the default layout for FormPanels and FieldSets. It extends AnchorLayout, so has all the sizing abilities of that layout. The key ability of FormLayout is that it is responsible for rendering labels next to form fields. If you configure a field with a fieldLabel, it will only appear if the field is in a layout which is using FormLayout. HBoxLayout Type: 'hbox'. This layout manager arranges child components horizontally across the Container. It uses hints in the child components to allocate available width in a proportional way. It does not wrap overflowing components onto another line. TableLayout Type: 'table'. This layout manager allows the developer to arrange child components in a tabular layout. A wrapping
element is created, and children are each rendered into their own
elements. TableLayout does not size the child components, but it accepts rowspan and colspan hints from the children to create a table of any complexity in which each cell contains a child component. VBoxLayout Type: 'vbox'. The vertical brother of 'hbox', this layout manager arranges child components one below the other, allocating available Container height to children in a proportional way. A dynamic application layout Our first example showed how an individual Container—a panel—which we programmatically render can arrange its child items according to a layout. To attain a desktop-like application layout, we will use the whole browser window. There is a special Container class called a Viewport which encapsulates the whole document body, and resizes itself whenever the browser changes size. If it is configured with a size-managing layout, that layout will then apply its sizing rules to any child components. A Viewport uses the as its main element. It does not need to be programmatically rendered. In the remaining layout examples, we will use a Viewport to create a fully dynamic application. Layouts [ 148 ] Our first Viewport The most common layout manager used in Viewports is the border layout. Applications frequently make use of border regions to represent header, navigation bar, footer, etc. In our examples we will use all five regions (don't forget that only the center region is required!). Border layouts offer split bars between regions to allow them to be resized. It also allows border regions to be collapsed, with the center region then expanding to fill the free space. This offers great flexibility to the user in arranging child components. Viewports may of course use any layout manager. However, we will be using a border layout in our Viewports from now on. All our regions will be panels. (Config objects with no xtype generally default to using a panel as the child component). As with all Containers, any component can be used as a child component. We make use of the automatic border display, and the header element which the panel class offers. In the child components of our border layout example, we specify several hints for the layout manager to use. These include: • region: The border to dock the child component to. • split: This is only used by the border regions, not the center region. If specified as true, the child component is rendered with a splitbar separating it from the center region, and allows resizing. Mouse-based resizing is limited by the minWidth/minHeight and maxWidth/maxHeight hint configs. • collapsible: This is only used by border regions. It specifies that the child component may collapse towards its associated border. If the child component is a panel, a special collapse tool is rendered in the header. If the region is not a panel (only panels have headers), collapseMode: 'mini' may be used (see below). • collapseMode: If specified as 'mini', then a mini collapse tool is rendered in the child component's splitbar. • margins: This specifies the margin in pixels to leave round any child component in the layout. The value is a space-separated string of numeric values. The order is the CSS standard of top, right, bottom, left. I recommend use of five pixel margins between border regions to provide visual separation for the user. Remember that where regions adjoin, only one of them needs a margin! This is illustrated in our first example. • cmargins: A margins specification to use when a region is collapsed. Chapter 7 [ 149 ] Running the first Viewport example, you should see the following user interface: Notice the margins providing visual separation of the regions. Also, notice the mini collapse tool in the splitbar of the west region. If you resize the browser, the north and south regions maintain their height, and east and west their width, while the center occupies the remaining space. Let's examine the code which created this sample layout: var viewport = new Ext.Viewport({ layout: "border", defaults: { bodyStyle: 'padding:5px;', }, items: [{ region: "north", html: 'North', margins: '5 5 5 5' },{ region: 'west', split: true, collapsible: true, collapseMode: 'mini', title: 'Some Info', width: 200, minSize: 200, Layouts [ 150 ] html: 'West', margins: '0 0 0 5' },{ region: 'center', html: 'Center', margins: '0 0 0 0' },{ region: 'east', split: true, width: 200, html: 'East', margins: '0 5 0 0' },{ region: 'south', html: 'South', margins: '5 5 5 5' }] }); All the child components are specified by simple config objects. They have a region hint and they use the html config which is from the Component base class. The html config specifies HTML content for the Component. This is just to indicate which region is which. There are better ways for the child Panels to be given content, which we will see later in this chapter. Take note of how the north region has a five pixel border all around. Notice then how all the regions under it (west, center and east) have no top border to keep consistent separation. Nesting: child components may be Containers In our third example, we will make the center region into something more useful than a panel containing simple HTML. We will specify the center child component as a TabPanel. TabPanel is a Container, so it has a single child component. A panel with some simple HTML in this case! The only difference in this example is that the center config object has an xtype which means it will be used to create a TabPanel (instead of the default panel), and it has an items config specifying child panels: Chapter 7 [ 151 ] { region: 'center', xtype: 'tabpanel', activeTab: 0, items: [{ title: 'Movie Grid', html: 'Center' }], margins: '0 0 0 0' } This results in the following display: The TabPanel class uses a card layout to show only one of several child components at once. Configure it with an activeTab config specifying which one to show initially. In this case it's our 'Movie Grid' panel. Of course the child component we have configured in our TabPanel is not a grid. Let's add the GridPanel we created in Chapter 5 to our TabPanel to demonstrate the card switching UI which the TabPanel offers: { region: 'center', xtype: 'tabpanel', bodyStyle: '', activeTab: 0, items: [{ Layouts [ 152 ] title: 'Movie Grid', xtype: 'grid', store: store, autoExpandColumn: 'title', colModel: new Ext.grid.ColumnModel({ columns: […] // not shown for readability }), view: new Ext.grid.GroupingView(), sm: new Ext.grid.RowSelectionModel({ singleSelect: true }) },{ title: 'Movie Descriptions' }], margins: '0 0 0 0' } In this example, we can see that the first child component in the TabPanel's items array is a grid. Take note of this to avoid a common pitfall encountered by first time Ext JS users. It can be tempting to overnest child components within extra Container layers, by placing a grid within a panel. Many that attempt to use a grid as a tab first time end up wrapping the grid within a panel like this: { xtype: 'tabpanel', items: [{ // Open Panel config with no layout items: [{ xtype: 'grid', // Panel contains a grid … }] }] } If you read the above code correctly, you can see why it is incorrect and inefficient, and will result in a buggy display. Recall that the default child Component type to create if no xtype is specified is a panel. So that single child Component of the TabPanel is a panel which itself contains a grid. It contains a grid, but it has no layout configured. This means that the grid will not be sized, and has an extra Component layer that serves no purpose. This problem is referred to as overnesting and when overnesting is combined with omission of layout configuration, results are unpredictable. Do not think of putting a grid in a tab, think of using a grid as a tab. Chapter 7 [ 153 ] Our correctly configured layout will result in the following display: In the above example, the Viewport has sized its center child—the TabPanel—and the TabPanel's card layout have sized its children. The grid is sized to fit as a tab. The second tab is not functional as yet. Let's move on to that next. Accordion layout Now that we have a functioning application of sorts, we can demonstrate another layout manager. An accordion is a familiar layout pattern in modern web applications. Multiple child components are stacked up vertically with their content areas collapsed. Only one may be expanded at a time. The accordion layout allows this to be set up very easily. Just use panels as children of a Container which uses the accordion layout, and the panel's headers are used as placeholders which may be clicked to expand the content area: { xtype: 'container', title: 'Movie Descriptions', layout: 'accordion', defaults: { border: false Layouts [ 154 ] }, items: [{ title: 'Office Space', autoLoad: 'html/1.txt' },{ title: 'Super Troopers', autoLoad: 'html/3.txt' },{ title: 'American Beauty', autoLoad: 'html/4.txt' }] } Notice that here, we are using xtype: 'container' as the accordion. We do not need borders, or toolbars, or a header, so we do not need the complex DOM structure of the panel class; the simple Container will do. The defaults config is applicable to any Container to provide default configs for child components. In this case, because the TabPanel provides its own borders, we do not want another layer of borders. Running this example code and selecting the Movie Descriptions tab will result in the following display: Chapter 7 [ 155 ] Clicking the headers in the accordion expands the panel's body, and displays the description which was loaded into the child panel using the autoLoad config option. This option may be used to load purely decorative HTML. Loading HTML in this way only loads HTML fragments, not full HTML documents. Scripts and stylesheets will not be evaluated. This option should not be used as a substitute for adding child components if Components are really what is required. A toolbar as part of the layout To make the application more useful, we can add a toolbar to the user interface to add buttons and menus. We encountered these classes in Chapter 4, Menus, Toolbars, and Buttons. We can use a toolbar as the north region of the Viewport. Again, note that we do not put the toolbar inside a north region; we use it as a north region to avoid the overnesting problem. To ease the clarity of code which creates the Viewport, we create the config object for the toolbar and reference it with a variable: var toolbarConfig = { region: 'north', height: 27, xtype: 'toolbar', items: [' ', { text: 'Button', handler: function(btn){ btn.disable(); } }, '->', { … }; The config object we create contains the expected region and xtype properties. Border layout requires that border regions be sized, so we give the toolbar a height of 27 pixels. The items in it, buttons, spacers, separators, fills, and input fields have been covered in Chapter 4. As with the toolbar examples in Chapter 4, the handling functions are members of a singleton object. In this case, we have an object referenced by a var Movies which offers methods to operate our application. Layouts [ 156 ] The resulting display will look like this: Using a FormPanel in the layout We can use that empty west region to do something useful now. We can use a FormPanel in the west region to edit the details of the selected movie. We will configure a listener on the rowselect event of the SelectionModel to load the selected movie record into the FormPanel: sm: new Ext.grid.RowSelectionModel({ singleSelect: true, listeners: { rowselect: Movies.loadMovieForm } }) The listener function is a member of the Movies object: loadMovieForm: function(sm, rowIndex, rec) { editRecord = rec; movieForm.getForm().loadRecord(rec); }, Chapter 7 [ 157 ] The selected record is assigned to the editRecord variable which is private to the Movies object. This is so that other member functions can use the record later. The getForm method of the FormPanel returns the BasicForm object which manages the input fields within a FormPanel. The loadRecord method of the BasicForm copies values from the data fields of the record into the form's input fields, using the form fields names to match up the data. Our example does not handle form submission yet, but it does update the record when you click the Submit button. The submit handler function is a member function of the Movies singleton: submitMovieForm: function(){ if (editRecord) { movieForm.getForm().updateRecord(editRecord); if (store.groupField && editRecord.modified[store.groupField]) { store.groupBy(store.groupField, true); } movieForm.getForm().submit({ success: function(form, action){ Ext.Msg.alert('Success', 'It worked'); }, failure: function(form, action){ Ext.Msg.alert('Warning', action.result.errormsg); } }); } }, First we update the record we are currently editing using the updateRecord method of the BasicForm. This will automatically update the movie's row in the grid. However if we modify the field that the grid is grouped by, the store will not detect this and regroup itself, so the above contains code to ensure that the grouping state is kept up to date. If the store is grouped and the modified property of the record (see the API docs for the Record class) contains a property named after the store's group field, then we explicitly tell the store to regroup itself. As usual, a modification of the store causes automatic update of the grid UI. Layouts [ 158 ] If we change the genre of the first movie, and Submit it, the resulting display will look like this: The form you see above shows how the FormLayout class will render labels attached to its child components. The FormLayout class injects into each child component, a reference to the Component's