| Summary: This is part 1 of a multi-part tutorial describing the layout widgets in Ext and giving functional examples of how to use them properly. |
| Author: Dave Fenwick |
| Published: May 01, 2007 |
| Ext Version: 1.1 |
Languages: English Chinese
|
The Ext layouts are extremely useful for building web applications with Ext. This is part 1 of a multi-part tutorial describing the layout engine, the region manager, how to create regions, and how to add panels to those regions.
Contents |
The layout engine was added in pre-alpha versions of Ext to handle cross-browser layout functionality. In his first and second blog entries about cross-browser web 2.0 layout functions, Jack Slocum discussed strategies for building an interface that allowed region management for specific regions defined by an encompassing region manager. Allowing border layout to the edge of a defined DOM element was a big step forward in enabling thick-like client web interface development with Ext.
The layout manager manages these regions. The principal user component of layout management is the BorderLayout class. This class provides an entry point for you to develop applications with the full rich interface provided with Ext. Layout is managed in a set of predefined regions. The available regions are north, south, east, west, and center. Every BorderLayout object provides these regions but only the center region must be used. If you have more panels than can fit in a single region, you can nest additional BorderLayout instances using the NestedLayoutPanel class.
Panels are the other component of region management. Panels provide an area for you to place your other Ext widgets, load HTML, embed IFrames, or do pretty much anything you'd normally do with HTML pages. Although the NestedLayoutPanel is a panel, it is a specialized panel that links a region layout to an additional BorderLayout. Additional panels include ContentPanel, GridPanel, and TreePanel.
Tutorial Note: Each of the files in the tutorial has a matching .html and .js file. As I progress through each portion of the tutorial, you can load these in your editor (they're provided in the .zip file located here) just to verify what's happening. If you only see one file listing here in the tutorial it's because the changes to the other file have been minimal. Clicking a .html file will open the page with the browser, so you can test the results. You can save individual .html or .js file by right-clicking the link. In addition, at the top of this tutorial is a zip file containing this web page, the style sheet for it, and all examples that are presented here.
The following is a simplified basic layout with a north, south, east, west, and center region with each region containing a ContentPanel and using splitters for each region:
In the above, the BorderLayout is the pseudo frame surrounding all of the other frames. The BorderLayout is sized to the DOM element passed to it as the first parameter to the constructor. Inside the BorderLayout are 5 ContentPanel objects. To build this interface, we simply instantiate the BorderLayout and then add ContentPanel objects to each region:
var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 50 }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100 }, center: { } });
This is a very basic layout that applies properties for the north, south, east, and west regions to allow splitters and setting initial sizes, and defines the center region for later layout. In this example, the BorderLayout is being bound to the DOM element document.body, but a BorderLayout can be bound to any element defined in your DOM that is a block tag. After defining the BorderLayout, we then add the ContentPanel objects to it (based on our example):
mainLayout.beginUpdate(); mainLayout.add('north', new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false })); mainLayout.add('east', new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false })); mainLayout.add('center', new Ext.ContentPanel('center-div', { fitToFrame: true })); mainLayout.endUpdate();
This brief example adds ContentPanel objects to all of the regions. It starts by calling mainLayout.beginUpdate(). beginUpdate() tells the BorderLayout object not to auto layout the objects being added to it until it reaches an endUpdate(). This improves the overall user experience by not flashing a bunch of user interface updates while the ContentPanel objects are being added to the layout. After calling beginUpdate(), it then adds the 5 ContentPanel objects to the regions. All ContentPanel objects, with the exception of the center region's ContentPanel, are set to not be closable. We'll leave the center region closable in case we want all pages in the center to be closed. We'll get into more of that in Part 2 of this tutorial. All of the ContentPanel objects are also set with the fitToFrame configuration option. This fits them to the frame of their parent element. Finally we call endUpdate() to render our layout.
An important note for Internet Explorer: The element that will contain your BorderLayout must have size in order for it to render properly. This typically isn't a problem if you're writing an application that uses the document.body since document.body always has size (well mostly anyway - if it didn't we wouldn't see anything in the browser.) But if you're using layout management within containers on an existing web page (perhaps in a DIV) you'll need to give that DIV size in order for the layout to render. Something like this works fine:
<div style="height:400px;"></div>
Let's put together a basic full layout just for continuity sake. This assumes Ext is a in subdirectory named ext-1.0 of the parent directory from your code.
<html> <head> <title>Simple Layout</title> <link rel="stylesheet" type="text/css" href="../ext-1.0/resources/css/ext-all.css"/> <script type="text/javascript" src="../ext-1.0/adapter/yui/yui-utilities.js"></script> <script type="text/javascript" src="../ext-1.0/adapter/yui/ext-yui-adapter.js"></script> <script type="text/javascript" src="../ext-1.0/ext-all.js"></script> <script type="text/javascript" src="simple.js"></script> </head> <body> <div id="north-div"></div> <div id="south-div"></div> <div id="east-div"></div> <div id="west-div"></div> <div id="center-div"></div> </body> </html>
Beginning with version 1.1, Ext includes a native Ext adapter, so the external libraries are no longer required (see FAQ). So in the above code as of ext-1.1 you can optionally just use ext-base.js as follows:
...
<script type="text/javascript" src="../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../ext-all.js"></script>
<script type="text/javascript" src="simple.js"></script>
...
Simple = function() { return { init : function() { var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 50 }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100 }, center: { } }); mainLayout.beginUpdate(); mainLayout.add('north', new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false })); mainLayout.add('east', new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false })); mainLayout.add('center', new Ext.ContentPanel('center-div', { fitToFrame: true })); mainLayout.endUpdate(); } }; }(); Ext.EventManager.onDocumentReady(Simple.init, Simple, true);
While the above shows the basics of building a layout, it isn't overly useful. There's nothing to do but move the splitters around. We need to add content to it. Content can be provided in one of several ways. If you add content directly into the DIV element your ContentPanel is bound to, the ContentPanel object will render whatever content you place inside that div. Let's try it out. We'll change the HTML to add some content to the DIV with the center-div id.
<html> <head> <title>Simple Layout</title> <link rel="stylesheet" type="text/css" href="../ext-1.0/resources/css/ext-all.css"/> <script type="text/javascript" src="../ext-1.0/adapter/yui/yui-utilities.js"></script> <script type="text/javascript" src="../ext-1.0/adapter/yui/ext-yui-adapter.js"></script> <script type="text/javascript" src="../ext-1.0/ext-all.js"></script> <script type="text/javascript" src="simple.js"></script> </head> <body> <div id="north-div"></div> <div id="south-div"></div> <div id="east-div"></div> <div id="west-div"></div> <div id="center-div"> This is some content that will display in a panel when a ContentPanel object is attached to the div. </div> </body> </html>
Another means of adding content to a ContentPanel object is to use the ContentPanel object's functions to load data. There are several functions available in the class to do this. Here we'll use 2 of them: setContent() and setUrl(). setContent() allows you to roll your own HTML within your Javascript application and add it to the ContentPanel. setUrl(), on the other hand, allows you to load content from an existing document on your server into a ContentPanel.
In our original example, the ContentPanel objects we created for our layout were anonymous. This is fine, but to reference them, you need to go through some hoops to get to the reference of that object through whichever region manager is assigned to the object. This isn't ideal, so here I'll be assigning variables to the ContentPanel objects so we can directly reference them within our codebase.
Simple = function() { var northPanel, southPanel, eastPanel, westPanel, centerPanel; return { init : function() { var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 50 }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100 }, center: { } }); mainLayout.beginUpdate(); mainLayout.add('north', northPanel = new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', southPanel = new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false })); mainLayout.add('east', eastPanel = new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', westPanel = new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false })); mainLayout.add('center', centerPanel = new Ext.ContentPanel('center-div', { fitToFrame: true })); mainLayout.endUpdate(); northPanel.setContent('This panel will be used for a header'); westPanel.setContent('<img src="nav-mini.gif" border="0" alt="Mini Layout Image" />'); centerPanel.setUrl('index.html'); centerPanel.refresh(); } }; }(); Ext.onReady(Simple.init, Simple, true);
We've now dynamically loaded content from an existing page (our tutorial HTML, no less). But there's a problem. Our center panel's content extends beyond the end of the page and there's no means to get to it. To fix this we have to provide some configuration properties to our ContentPanel object. By adding the autoScroll configuration option to our panel while using the fitToFrame configuration property, when our content exceeds the width or height of the DOM element it's tied to, the ContentPanel will automatically provide scrollbars for it.
There's also another problem with our layout. In Internet Explorer the styling for our center panel isn't right. The problem we have here is that some browsers support dynamically loading stylesheets into a content region and some don't. To overcome this the IFRAME tag was invented. An IFRAME can be considered a complete browser implementation including loading scripts, style sheets, what-have-you, all into a specific block element in your document. The benefit of an IFRAME is that it segregates itself from your page, so if you override styles in your IFRAME that are used on your parent page, it won't affect your parent page's styles. If, however, you know the content you're loading only deals with body elements, you don't have to use an IFRAME for your content. In our example, though, I know we have a stylesheet associated with the document we're loading into our center panel and therefore we're using an IFRAME to handle it properly.
IFRAME tags can get a little tricky with how they do layout, and rather than using the ContentPanel object to load the IFRAME, we're going to load it manually into the DOM. In addition, we're going to change which element the center ContentPanel uses to fit itself to the frame. By doing this we can use the IFRAME as the container we'll fit our panel to, which will then fill the center region.
Let's setup autoScroll and put the center content into an IFRAME.
<html> <head> <title>Simple Layout</title> <link rel="stylesheet" type="text/css" href="../ext-1.0/resources/css/ext-all.css"/> <script type="text/javascript" src="../ext-1.0/adapter/yui/yui-utilities.js"></script> <script type="text/javascript" src="../ext-1.0/adapter/yui/ext-yui-adapter.js"></script> <script type="text/javascript" src="../ext-1.0/ext-all-debug.js"></script> <script type="text/javascript" src="simple4.js"></script> </head> <body> <div id="north-div"></div> <div id="south-div"></div> <div id="east-div"></div> <div id="west-div"></div> <div id="center-div"> <iframe id="center-iframe" frameborder="0" scrolling="auto" style="border:0px none;"></iframe> </div> </body> </html>
Simple = function() { var northPanel, southPanel, eastPanel, westPanel, centerPanel; return { init : function() { var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 50 }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100 }, center: { } }); mainLayout.beginUpdate(); mainLayout.add('north', northPanel = new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', southPanel = new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false })); mainLayout.add('east', eastPanel = new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', westPanel = new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false })); mainLayout.add('center', centerPanel = new Ext.ContentPanel('center-div', { fitToFrame: true, autoScroll: true, resizeEl: 'center-iframe' })); mainLayout.endUpdate(); northPanel.setContent('This panel will be used for a header'); Ext.get('center-iframe').dom.src = 'index.html'; } }; }(); Ext.EventManager.onDocumentReady(Simple.init, Simple, true);
Now we're getting somewhere. For the most part our scrollbars should be working now. On a side note, in Internet Explorer prior to version 7, if the document you present to an IFRAME contains a fully specified DOCTYPE tag at the top that includes a URL for the DTD of the DOCTYPE, IE will probably display a horizontal scrollbar if a vertical scrollbar is present. This is a bug with the peek-ahead code in the IE layout.
Now we have a very basic layout with ContentPanel objects. Let's add a toolbar to our center ContentPanel object. Creating a toolbar is pretty simple. I'm not going to go into details on creating the toolbar object itself because it's a bit outside the scope of this discussion. What we're after here is how to attach a toolbar to a ContentPanel object. Here's a basic toolbar creation:
var simpleToolbar = new Ext.Toolbar('simple-tb'); simpleToolbar.addButton({ text: 'Scroll Bottom', cls: 'x-btn-text-icon scroll-bottom'}); simpleToolbar.addButton({ text: 'Scroll Top', cls: 'x-btn-text-icon scroll-bottom'});
To support the toolbar, we need to add a tag to our HTML, we need to add some code to create the toolbar in our Javascript, and we need to bind it to our center ContentPanel object. Our toolbar will have 2 buttons: Scroll Bottom and Scroll Top. These buttons will scroll the IFRAME content to the bottom or the top of the document. In order to accomplish this, we'll also need to add a function that gets the IFRAME document element that is cross-browser capable. Let's do that.
<html> <head> <title>Simple Layout</title> <link rel="stylesheet" type="text/css" href="../ext-1.0/resources/css/ext-all.css"/> <script type="text/javascript" src="../ext-1.0/adapter/yui/yui-utilities.js"></script> <script type="text/javascript" src="../ext-1.0/adapter/yui/ext-yui-adapter.js"></script> <script type="text/javascript" src="../ext-1.0/ext-all-debug.js"></script> <script type="text/javascript" src="simple5.js"></script> <style> .scroll-bottom .x-btn-text { background-image: url(scroll-bottom.gif); } .scroll-top .x-btn-text { background-image: url(scroll-top.gif); } </style> </head> <body> <div id="north-div"></div> <div id="south-div"></div> <div id="east-div"></div> <div id="west-div"></div> <div id="center-div"> <div id="center-tb"></div> <iframe id="center-iframe" frameborder="0" scrolling="auto" style="border:0px none;"></iframe> </div> </body> </html>
function getIframeDocument(el) { var oIframe = Ext.get('center-iframe').dom; var oDoc = oIframe.contentWindow || oIframe.contentDocument; if(oDoc.document) { oDoc = oDoc.document; } return oDoc; } Simple = function() { var northPanel, southPanel, eastPanel, westPanel, centerPanel; return { init : function() { var simpleToolbar = new Ext.Toolbar('center-tb'); simpleToolbar.addButton({ text: 'Scroll Bottom', cls: 'x-btn-text-icon scroll-bottom', handler: function(o, e) { var iframeDoc = getIframeDocument('center-iframe'); iframeDoc.body.scrollTop = iframeDoc.body.scrollHeight; } }); simpleToolbar.addButton({ text: 'Scroll Top', cls: 'x-btn-text-icon scroll-top', handler: function(o, e) { var iframeDoc = getIframeDocument('center-iframe'); iframeDoc.body.scrollTop = 0; } }); var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 50 }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100 }, center: { } }); mainLayout.beginUpdate(); mainLayout.add('north', northPanel = new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', southPanel = new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false })); mainLayout.add('east', eastPanel = new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', westPanel = new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false })); mainLayout.add('center', centerPanel = new Ext.ContentPanel('center-div', { fitToFrame: true, autoScroll: true, resizeEl: 'center-iframe', toolbar: simpleToolbar })); mainLayout.endUpdate(); northPanel.setContent('This panel will be used for a header'); Ext.get('center-iframe').dom.src = 'index.html'; } }; }(); Ext.EventManager.onDocumentReady(Simple.init, Simple, true);
Now that we have a somewhat reasonable layout, let's add some decorations to the regions. Regions can have titlebars. This allows visual identification of the purpose for the region. To add a titlebar to a region, in the configuration properties for the region you specify a titlebar property. Then, when you create the panel for that region, you specify the title property for that panel. Pretty simple.
All regions except for the center region can also be collapsed and expanded. To enable a region to be collapsed, you specify the collapsible property in the region configuration. When you do this, you'll probably also want to specify a titlebar and collapsedTitle on the region and a title on the panel for that region. The collapsedTitle is the text that's displayed for that region when the region is collapsed. The collapsedTitle property is only available on the north and south regions.
Let's add a titlebar to the center, south, and west regions and make the west and south regions collapsible. We'll also put a collapsedTitle on the south region's titlebar.
function getIframeDocument(el) { var oIframe = Ext.get('center-iframe').dom; var oDoc = oIframe.contentWindow || oIframe.contentDocument; if(oDoc.document) { oDoc = oDoc.document; } return oDoc; } Simple = function() { var northPanel, southPanel, eastPanel, westPanel, centerPanel; return { init : function() { var simpleToolbar = new Ext.Toolbar('center-tb'); simpleToolbar.addButton({ text: 'Scroll Bottom', cls: 'x-btn-text-icon scroll-bottom', handler: function(o, e) { var iframeDoc = getIframeDocument('center-iframe'); iframeDoc.body.scrollTop = iframeDoc.body.scrollHeight; } }); simpleToolbar.addButton({ text: 'Scroll Top', cls: 'x-btn-text-icon scroll-top', handler: function(o, e) { var iframeDoc = getIframeDocument('center-iframe'); iframeDoc.body.scrollTop = 0; } }); var mainLayout = new Ext.BorderLayout(document.body, { north: { split: true, initialSize: 50 }, south: { split: true, initialSize: 125, titlebar: true, collapsedTitle: 'Status', collapsible: true }, east: { split: true, initialSize: 100 }, west: { split: true, initialSize: 100, titlebar: true, collapsible: true }, center: { titlebar: true} }); mainLayout.beginUpdate(); mainLayout.add('north', northPanel = new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false })); mainLayout.add('south', southPanel = new Ext.ContentPanel('south-div', { fitToFrame: true, closable: false, title: 'Status' })); mainLayout.add('east', eastPanel = new Ext.ContentPanel('east-div', { fitToFrame: true, closable: false })); mainLayout.add('west', westPanel = new Ext.ContentPanel('west-div', { fitToFrame: true, closable: false, title: 'Navigation' })); mainLayout.add('center', centerPanel = new Ext.ContentPanel('center-div', { fitToFrame: true, autoScroll: true, resizeEl: 'center-iframe', toolbar: simpleToolbar, title: 'Content' })); mainLayout.endUpdate(); northPanel.setContent('This panel will be used for a header'); Ext.get('center-iframe').dom.src = 'index.html'; } }; }(); Ext.EventManager.onDocumentReady(Simple.init, Simple, true);
Notice that when we collapse the west region there's no title on the west region's layout bar. HTML currently does not provide a way to rotate text 90 degrees in an element. To rectify this, we can provide our own by styling that bar with an image that contains the text 'Navigation'. I created an image that is 18 pixels wide by 150 pixels high and added the word 'Navigation' to the center of it rotated 90 degrees to the left with an obscure color fill, using that color as the transparency color (pretty typical web stuff.)
In order for the image to show up, we need to augment one of the core Ext widget styles with our own style for the background. It's as simple as adding a style to your stylesheet or in your HTML head for the .x-layout-collapsed-west style. Here's the style I added to simple7.html to get the styles in appropriately:
.x-layout-collapsed-west { background-image: url(navigation.gif); background-repeat: no-repeat; background-position: center; }
In part 2 of this tutorial we'll learn the advanced ins and outs of the layout manager, including creating nested layouts, programmatically expanding and collapsing regions, creating tabbed layouts, and other assorted sundries useful for creating EXTellent applications.
The Ext documentation is probably your best source of good information. In addition to the 2 blog posts I mentioned at the beginning of this article, the Ext examples in the documentation suite are also a great source of information. The Ext forums are yet another great source of information with a lot of knowledge stored. Finally, there is an IRC channel on freenode.net named #extjs that is usually filled with people that are willing to help out if you need help.