Monday, November 9, 2009

SectionStack with +/- icon

Working on my latest project, I wanted a sectionStack on the left side of the window. Having been exposed to Outlook too long, I didn't like the +/- icons shown on the section headers. Searching the SmartClient forums didn't result in a workaround. Setting canCollapse: false will hide the icon but then clicking on the header doesn't do anything. After many attempts, I settled on the following. Set canCollapse: false on each section and handle the click event to trigger the default header click action that occurs when canCollapse is true.

canCollapse:false,
click : function() {
var layout = this.getLayout();
if (!layout) return;
return layout.sectionHeaderClick(this);
}

Monday, September 28, 2009

Masked TextItem in SmartGWT

In a previous blog entry, Masked TextItem, I described an enhanced version of the standard SmartClient TextItem that supported various text masks. A refined version of that code has now been integrated into the main line of the SmartClient sources. It is likely to be released as part of 7.1 (or the next release after 7.0). However, because the SmartGWT project is based on the very latest builds this feature is now available in SmartGWT 1.3. Check it out.

Pretty Cool!

Wednesday, May 20, 2009

SmartClient quickstart tutorial: News Reader

Introduction
This tutorial is meant to provide an introduction to SmartClient fundamentals by building a small news reader application. The application displays the list of articles from an RSS feed at the top and below the selected article is displayed. The amount of code needed is surprisingly small.



Install SmartClient SDK
Start by downloading the latest SmartClient SDK from http://www.smartclient.com/releases/SmartClient_70RC_LGPL.zip. This is the 7.0 Release Candidate, however, all code should apply to release 6.5.1 as well.

To install the SDK just unpack the zip archive into a convenient location. The location I chose is C:\SmartClient_70RC. Once unpacked, run the batch file C:\SmartClient_70RC\smartclientSDK\start_embedded_server.bat to start the local Tomcat server on port 8080 for testing and local documentation access.

Point your browser to http://localhost:8080 and select Getting Started->Feature Explorer to see some of the cool features of SmartClient. The API reference is also available at Docs->SmartClient Reference.

Now you are ready to start developing your own SmartClient applications.

Template
All SmartClient applications are hosted within a (single) web page. Certain libraries are required and must be loaded in a specific order. For the purposes of this article the web page holding our app will be placed in the C:\SmartClient_70RC\smartclientSDK directory.

Open notepad and create newsreader.html as follows:


<html>
<head>
<title>SmartClient Wired News Reader</title>

<!------ References to SmartClient Objects ------->
<script>var isomorphicDir="isomorphic/";</script>
<script src=isomorphic/system/modules/ISC_Core.js></script>
<script src=isomorphic/system/modules/ISC_Foundation.js></script>
<script src=isomorphic/system/modules/ISC_Containers.js></script>
<script src=isomorphic/system/modules/ISC_Grids.js></script>
<script src=isomorphic/system/modules/ISC_Forms.js></script>
<script src=isomorphic/system/modules/ISC_DataBinding.js></script>
<script src=isomorphic/system/development/ISC_FileLoader.js></script>
<script src=isomorphic/skins/TreeFrog/load_skin.js></script>
</head>

<body>
<script>

// SmartClient application goes here

</script>
</body>

</html>


Just to make sure the page is correct, point your browser to http://localhost:8080/newsreader.html. If you see a nice empty page you probably have a good template.

Hello, World!
Now we are ready to see SmartClient in action. Let's start with the standard Hello, World! application. Between the script tags at the bottom, enter the following:

isc.Label.create({
contents: "<i>Hello, World!</i>"
})


Here a label is created whose contents is Hello, World! in italics. The contents property holds the HTML to be displayed in the label. Give this a try by refreshing your browser.



DataSource
A DataSource is a SmartClient object that describes basic CRUD access to server data. We will be reading from an RSS feed so a dataSource that can read and parse an XML document into records and fields is what we need. For this tutorial, we will be reading from the Wired.com feed at http://feeds.feedburner.com/wired. The format of the data we want looks like the following:

<rss>
<channel>
<title>Wired Top Stories</title>
<link>http://www.wired.com/rss/index.xml</link>
<description>Top Stories</description>
<language>en-us</language>
<copyright>Copyright 2007 CondeNet Inc. All rights reserved.</copyright>
<pubDate>Tue, 19 May 2009 15:32:00 GMT</pubDate>
<category />
...

<item>
<title>Review: &lt;cite&gt;Punch-Out!!&lt;/cite&gt; Is an Absolute Old-School Knockout</title>
<link>http://feeds.wired.com/~r/wired/index/~3/oEop7fjF6OA/</link>
<description>Punch-Out!! is an awesome new Wii game that revives the classic 80's boxing game franchise. An excellent game for hardcore and casual players alike.
&lt;p&gt;&lt;a href=" net="" at="" mey98bmqydiesj8t8smbb0m5xz8="" 0="" da="" img="" src="http://feedads.g.doubleclick.net/%7Eat/MEy98bMQyDiEsj8t8SmBB0m5XZ8/0/di" border="0" ismap="true" br="">
&lt;a href="http://feedads.g.doubleclick.net/~at/MEy98bMQyDiEsj8t8SmBB0m5XZ8/1/da"&gt;&lt;img src="http://feedads.g.doubleclick.net/~at/MEy98bMQyDiEsj8t8SmBB0m5XZ8/1/di" border="0" ismap="true"&gt;&lt;/img&gt;&lt;/a&gt;&lt;/p&gt;&lt;img src="http://feeds2.feedburner.com/~r/wired/index/~4/oEop7fjF6OA" height="1" width="1"/&gt;</description>
<pubDate>Tue, 19 May 2009 15:32:00 GMT</pubDate>
<guid isPermaLink="false">http://www.wired.com/gamelife/2009/05/punch-out-review/</guid>
</item>
<item>
...
</item>
</channel>
</rss>


Great. Except for one issue we might as well deal with now: cross-site restrictions.

Cross-site access
So what's the problem? Well, to prevent scripts running in your local browser from doing things that shouldn't, scripts are limited to accessing the same server that provides the web page hosting it. See Same Origin Policy on Wikipedia for more details.

That's pretty limiting because how can a news reader we write and host locally access an RSS feed from Wired? There is a solution in Yahoo! Query Language (YQL). See more details at http://ajaxian.com/archives/yql-converting-the-web-to-json-with-mock-sql and http://developer.yahoo.com/yql/. Using YQL we can request that the RSS feed we want be converted from XML to JSON.

Wait a minute. How does converting the XML to JSON help us? SmartClient provides a special DataSource called XJSONDataSource that can pull JSON data from any site. So, if cross-site scripting is not allowed, how can this data source get around that limitation. The trick is that JSON is valid JavaScript code so the JSON response can be processed with a callback to convert it into an object.

I won't go into details on JSON or XJSONDataSource here because this is not something you will be doing in a production application.

Using YQL a query can be built to return details we want from the Wired RSS feed as a JSON message. Here is our YQL query:

http://query.yahooapis.com/v1/public/yql?q=select title,pubDate,link,description from rss where url%3D"http%3A%2F%2Ffeeds.feedburner.com%2Fwired"&format=json


The URL looks pretty complicated but if you are familiar with SQL it's pretty simple. Basically select four fields from the RSS feed at feeds.feedburner.com and return them in JSON format. The escaping makes it look messy.

The results look something like:

{
"query": {
"count":"30",
"created":"2009-05-19T07:56:05Z",
"lang":"en-US",
"updated":"2009-05-19T07:56:05Z",
"uri":"http://query.yahooapis.com/v1/yql?q=select+title%2CpubDate%2Clink%2Cdescription+from+rss+where+url%3D%22http%3A%2F%2Ffeeds.feedburner.com%2Fwired%22",
"diagnostics": {
"publiclyCallable":"true",
"url": {
"execution-time":"328",
"content":"http://feeds.feedburner.com/wired"
},
"user-time":"341",
"service-time":"328",
"build-version":"1432"
},
"results": {
"item": [
{ "title":"ACLU Sues School Districts for Blocking Gay-Rights Websites",
"link":"http://feeds.wired.com/~r/wired/index/~3/8K7UBiGeDyQ/",
"description":"School districts representing thousands of Tennessee ...",
"pubDate":"Tue, 19 May 2009 17:00:00 GMT"
},
{ "title":"40 Years After Apollo, 'Peanuts' Still Seeing Stars",
"link":"http://feeds.wired.com/~r/wired/index/~3/kEOSwobq4PM/",
"description":"Four decades ago this week, Snoopy and Charlie Brown ...",
"pubDate":"Tue, 19 May 2009 15:36:00 GMT"
}
]
}
}
}



Now that we have a URL and data to process we can put together a DataSource.

// Our news feed dataSource
//
isc.XJSONDataSource.create({
ID:"newsFeed",
recordXPath: "/query/results/item",
dataURL:'http://query.yahooapis.com/v1/public/yql?q=select title,pubDate,link,description from rss where url%3D"http%3A%2F%2Ffeeds.feedburner.com%2Fwired"&format=json',
fields:[
{ name: "title" },
{ name: "pubDate" },
{ name: "link", type: "link" },
{ name: "description" }
]
});


Let's take a look at this object definition. We create an instance of XJSONDataSource and name the instance newsFeed. This ID is important because we will use it to uniquely reference this object instance. In the SmartClient object structure, each object must have a unique ID. If you omit the ID, SmartClient will create one for you and the .create() call will return it for your future reference.

The dataURL is our YQL query. Looking back at the output from YQL you will note that each record is a new item. Viewing the JSON output like it was XML we could write an Xpath to each record as "/query/results/item" and that's exactly what we set in the data source recordXPath property. Finally we define the fields in each record specifying the field name and optionally its type. Here the type link is used to let SmartClient know that the string in the link field can be shown as a link in the browser. Otherwise it will be treated as a regular text field.

Article List
Using our data source created above, let's display the list of articles found in a grid. First, remove the Hello, World! label creation from our web page and add the data source.

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date" },
{ name: "link" }
]
});


Refresh your browser. If all went well you now see a screen showing a list of articles:



Because of the default SmartClient handling of link fields you can click on any article link and it will be displayed in another window or tab.

What does this code do? The width setting was used to tall the grid to use all of the width you can. In this case it uses the full width of the browser window.

The SmartClient ListGrid is a DataBoundComponent. That means it knows how to interact with DataSources automatically. By linking the ListGrid with the newsFeed dataSource, the ListGrid can fetch records as needed. Setting autoFetchData to true tells the grid to attempt to load records from the dataSource when first rendered.

Finally, a list of fields to display is provided. We don't want to include the description field because it can be very large. Just those fields that we want shown are included. Note that the order specified on the ListGrid dictates the display order no matter what order the fields are defined in the dataSource. A different title is given to the publish date because pubDate is not very descriptive. We will return to the fields later to show more features.

Layout
We now have an article list with very little code - really just object instantiations. Now lets add a little bit of polish. A header above the list would be helpful.

Here is a header that displays "Wired" on the left and "News Reader" on the right in a blue background.

isc.HLayout.create({
ID: "newsreaderHead",
width: "100%",
height: 50,
backgroundColor: "navy",
members: [
isc.Label.create({
contents: "Wired",
align: "left",
overflow: "hidden",
width: "50%",
className: "readerHeaderText"
}),
isc.Label.create({
contents: "News Reader",
align: "right",
overflow: "hidden",
width: "50%",
className: "readerHeaderText"
})
]
});


Add this code before the ListGrid create because we want it above the grid. Additionally we need to define a new style class to control the font, color and size of our text. Add this style definition in the page header just below the last <script src=...> line.

<style type="text/css">
.readerHeaderText {
color: white;
font-family: verdana,arial,helvetica,sans-serif;
font-size: 20px;
line-height: 24px;
padding: 4px;
}
</style>


Now refresh your browser window. But where is the header? If you just define some components on the page and don't use any form of layout manager each component will be shown fixed at position 0,0. That's what's happening here. The grid is on top of the header.

Before we fix this issue, let's look at the header definition above. To place two labels side by side, I have used an HLayout object - horizontal layout. This component holds one or more components and lays them out horizontally. I want the header to take up the entire row (width: "100%"), be 50 pixels high and have a navy background color. Then it should manage two labels each taking up 50% of the HLayout width. Note the use of className to specify a style class name for the component. Using styles is the way to control output directly. In this case, font, color and size of the text in the label.

So how do we put the header above the grid? How about a VLayout? Yep. VLayout manages that layout of multiple components in a vertical manner. Putting together a VLayout yields something like:

isc.VLayout.create({
width:"100%",
height:"100%",
members:[
"newsreaderHead",
"articleList"
]
});


Add this code after the ListGrid creation and refresh your browser window. Wow, that works! Notice that the article list now extends to the bottom of the window. This is because VLayout manages the heights of each member automatically. For the newsreaderHead, we defined an explicit height of 50 pixels so VLayout doesn't change that. But for the article grid there is no height specified so the VLayout gave it the remainder of the window.



Debugging interlude
There is something subtle going on behind the scenes that needs to be fixed. To see this problem let's take a look at an extremely powerful feature of SmartClient: the Developer Console. The console can be launched from any development or production SmartClient application at any time. To do so, type javascript:isc.showConsole(); into the browser address bar and press Enter. The console will be displayed in another browser window.



Note that it shows a log of events that have already occurred. Note that there are a number of messages similar to the following:


22:56:21.615:WARN:HLayout:newsreaderHead:Adding already drawn widget:isc_Label_0 to new parent:newsreaderHead. Child has been cleared so it can be drawn inside the new parent. This may be a result of autoDraw being enabled for the child.


What does this mean? Well, when creating a new component SmartClient automatically draws it on the page. Then in our case we placed the component (newsreaderHead in this case) within an HLayout component that changes its location and other characteristics. SmartClient redraws the component for us. This means the newsreaderHead is drawn twice (at least) every time the application is started or refreshed. That's not very efficient.

The auto-drawing feature is very nice for simple pages but needs to be disabled for more complicated layout-managed applications. So let's do that by adding a new property to our components: autoDraw: false. We can do this on all components but the final VLayout because that is the one we want to trigger the drawing.

Now refresh your newsreader browser window and take a look at the developer console. It should now show none of the redraw messages.

The developer console is very handy for watching RPC messages, evaluating test code and for logging your own event for debugging. We'll see more on this later in the tutorial.

Review
Here is the full page code so far. Note that we haven't written a single line of functional code - everything so far is completely declarative.

<html>
<head>
<title>SmartClient Wired NewsReader</title>

<!------ References to SmartClient Objects ------->
<script>var isomorphicDir="isomorphic/";</script>
<script src=isomorphic/system/modules/ISC_Core.js></script>
<script src=isomorphic/system/modules/ISC_Foundation.js></script>
<script src=isomorphic/system/modules/ISC_Containers.js></script>
<script src=isomorphic/system/modules/ISC_Grids.js></script>
<script src=isomorphic/system/modules/ISC_Forms.js></script>
<script src=isomorphic/system/modules/ISC_DataBinding.js></script>
<script src=isomorphic/system/development/ISC_FileLoader.js></script>
<script src=isomorphic/skins/TreeFrog/load_skin.js></script>

<style type="text/css">
.readerHeaderText {
color:white;
font-family: verdana,arial,helvetica,sans-serif;
font-size: 20px;
line-height: 24px;
padding: 4px;
}
</style>
</head>

<body>
<script>

// Our news feed dataSource
//
isc.XJSONDataSource.create({
ID:"newsFeed",
recordXPath: "/query/results/item",
dataURL:'http://query.yahooapis.com/v1/public/yql?q=select title,pubDate,link,description from rss where url%3D"http%3A%2F%2Ffeeds.feedburner.com%2Fwired"&format=json',
fields:[
{ name: "title" },
{ name: "pubDate" },
{ name: "link" },
{ name: "description" }
]
});

// UI Components
//

// Header
isc.HLayout.create({
ID: "newsreaderHead",
width: "100%",
height: 50,
backgroundColor: "navy",
autoDraw: false,
members: [
isc.Label.create({
contents: "Wired",
align: "left",
overflow: "hidden",
width: "50%",
autoDraw: false,
className: "readerHeaderText"
}),
isc.Label.create({
contents: "News Reader",
align: "right",
overflow: "hidden",
width: "50%",
autoDraw: false,
className: "readerHeaderText"
})
]
});

// Article list
isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date" },
{ name: "link" }
]
});

// UI Layout
isc.VLayout.create({
width:"100%",
height:"100%",
members:[
"newsreaderHead",
"articleList"
]
});

</script>
</body>

</html>


Show article text
Everything is looking great in the app but it would be much nicer to see the article text on the same page and not have to close a popup window. So let's put an article text pane below the list grid. Looking through the SmartClient reference an HTMLPane looks to be the perfect component for displaying the article. It allows scrolling when the content is too large to show in its entirety. So let's add the pane.

isc.HTMLPane.create({
ID:"articlePane",
width: "100%",
showEdges:false,
contentsType:"page",
autoDraw: false
});


Insert the code above before the VLayout. Then update the VLayout to add the pane to the list of managed members:

isc.VLayout.create({
width:"100%",
height:"100%",
members:[
"newsreaderHead",
"articleList",
"articlePane"
]
});


Great. We now have a pane to display HTML content. When the user clicks on a row (record) in the article list, the page referenced in the link field should be shown in the article pane. To do that we will implement an event handler on the ListGrid - our first active code.


isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date" },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
articlePane.setContentsURL(record.link, "page");
}

});


Looking at the SmartClient reference documentation we find the recordClick event is called whenever the user clicks on a record in the grid. That's what we want. Note the passed arguments give us access to various context-specific values allowing flexibility in responding to the event. For this case we just need to grab the link field from the record and tell the articlePane to display it. articlePane is the ID of our HTMLPane and is available as a global variable by default.

Refresh your browser window and click on a record in the article list. It should now show in the article pane. However, note that if you click in the link field a popup still occurs. To disable this "feature" just remove the type: "link" property from the dataSource field. The field is then treated as regular text.

What if you want to show the first article instead of a blank article pane at startup? The ListGrid raises an event dataArrived each time data arrives from the server. This could occur multiple times if the there are a lot records and only a portion are loaded at a time (paging).

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date" },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
articlePane.setContentsURL(record.link, "page");
},
dataArrived : function(startRow, endRow) {
if (startRow == 0) {
articlePane.setContentsURL(articleList.getRecord(0).link, "page");
}
}

});


When the startRow is 0, we will display the first article. Now let's take a look at another way to handle this event. Take a look at the dataArrived event in the SmartClient reference. Notice the label [String Method]. This means the event can be written as a normal method as shown above or as a string. Here is the same code implemented as a string method:

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date" },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
articlePane.setContentsURL(record.link, "page");
},
dataArrived : 'if (startRow == 0) { articlePane.setContentsURL(articleList.getRecord(0).link, "page");}'

});


The parameters for the event are available as normal variables in the string. For very simple handlers a string method is typically more compact. As a matter of personal preference I tend to use actual methods for all my event handlers.

Fine tuning
We now have a fully functional news reader with very little code. Now all we need to do is to tweak the user interface to be a bit more professional. Take a look at the article list. There are three columns all the same size. The publish date doesn't need to be as large as the other two. Let's define the width of the publish date as a fixed size and let the other two have the remaining space. Update the article list ListGrid fields as follows:

fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date", width: 200 },
{ name: "link" }
],


Now no matter how the browser window is resized, the publish date column is fixed at 200 pixels wide.

Do you remember when we defined the dataSource that we specified a description field? This is a summary of the article and we haven't used it yet. Let's use the description as a tooltip over the article in the article list. SmartClient calls a tooltip a hover.

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date", width: 200 },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
articlePane.setContentsURL(record.link, "page");
},
canHover: true,
cellHoverHTML : function(record, rowNum, colNum) {
return record.description;
}
,
dataArrived : 'if (startRow == 0) { articlePane.setContentsURL(articleList.getRecord(0).link, "page");}'
});


Add an event handler for cellHoverHTML to the articleList ListGrid as shown above. With this even handler a different tooltip (hover) can be shown for each row and/or cell. In this example, the same hover is shown for any location on the row: the record.description. The value returned from this event handler can be HTML formatted text. It turns out the description is HTML so that feature fits perfectly.

Refresh your browser window and pause the mouse over different records and take a look at the hover.

Lastly, let's look at allowing the vertical size of the article list and pane to be adjusted with a resize bar.

VLayout and HLayout support a resize bar automatically. Each component in the members can provide a property showResizeBar to tell the layout to display a resize bar after the component. This property is not meaningful to the member component, only the parent layout.

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date", width: 200 },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
articlePane.setContentsURL(record.link, "page");
},
canHover: true,
cellHoverHTML : function(record, rowNum, colNum) {
return record.description;
},
dataArrived : 'if (startRow == 0) { articlePane.setContentsURL(articleList.getRecord(0).link, "page");}',
height: "30%",
showResizeBar:true

});


I added the height setting to limit the default height of the article list to only 30% of the available space leaving more for the article text itself.

Debugging revisited
Earlier you saw some of the power of the debugging console. What if you could use the debugging console for your own debugging? Well you can log anything to the console you want to. A typical JavaScript developer uses alert() calls frequently for debugging. These are inefficient and are limited in usefulness especially in loops and for event handlers.

SmartClient provides some nice logging features you can use whenever you would use alert(). Let's add a couple of logging calls to our news reader to see the results.

isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date", width: 200 },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
isc.logEcho(record, "recordClick: ");
articlePane.setContentsURL(record.link, "page");
},
canHover: true,
cellHoverHTML : function(record, rowNum, colNum) {
isc.logWarn("hover: " + record.description);
return record.description;
},
dataArrived : 'if (startRow == 0) { articlePane.setContentsURL(articleList.getRecord(0).link, "page");}',
height: "30%",
showResizeBar:true
});


Start the debugging console if you closed it earlier and refresh your browser window. isc.logWarn writes a message to the log at warning level, the default display level. isc.logEcho logs a message and also the contents of an object instance. For example, here is a recordClick log entry:

09:09:00.530:MUP4:WARN:Log:recordClick: : {title:" music="" labels="" want="" pirate="" bay="" shuttered="">
link: "http://feeds.wired.com/~r/wired/index/~3..."[53],
description: "The recording industry wants to add new ..."[749],
pubDate: "Tue, 19 May 2009 20:13:00 GMT",
_selection_1: true}


These logging methods are extremely helpful in tracking down timing or sequence issues without interfering with the UI.

Conclusion
I hope this basic tutorial has helped you see some of the power of SmartClient architecture. This example didn't even touch on the date entry aspects of the framework nor on the powerful search and filter capabilities of the ListGrid. Browse through the feature explorer for examples of many of the other SmartClient features.

One final tip, when putting together a layout or testing a small piece of code, rather than using your template page pick any JavaScript example in the Feature Explorer and completely replace the code on the JS tab with your test code and click Try It. Or you can paste the test code into the debugging console in the Evaluate JS Expression box and click the Eval JS button.

Full final newsreader source


<html>
<head>
<title>SmartClient Wired NewsReader</title>

<!------ References to SmartClient Objects ------->
<script>var isomorphicDir="isomorphic/";</script>
<script src=isomorphic/system/modules/ISC_Core.js></script>
<script src=isomorphic/system/modules/ISC_Foundation.js></script>
<script src=isomorphic/system/modules/ISC_Containers.js></script>
<script src=isomorphic/system/modules/ISC_Grids.js></script>
<script src=isomorphic/system/modules/ISC_Forms.js></script>
<script src=isomorphic/system/modules/ISC_DataBinding.js></script>
<script src=isomorphic/system/development/ISC_FileLoader.js></script>
<script src=isomorphic/skins/TreeFrog/load_skin.js></script>

<style type="text/css">
.readerHeaderText {
color:white;
font-family: verdana,arial,helvetica,sans-serif;
font-size: 20px;
line-height: 24px;
padding: 4px;
}
</style>
</head>
<body>
<script>

// Our news feed dataSource
//
isc.XJSONDataSource.create({
ID:"newsFeed",
recordXPath: "/query/results/item",
dataURL:'http://query.yahooapis.com/v1/public/yql?q=select title,pubDate,link,description from rss where url%3D"http%3A%2F%2Ffeeds.feedburner.com%2Fwired"&format=json',
fields:[
{ name: "title" },
{ name: "pubDate" },
{ name: "link" },
{ name: "description" }
]
});

// UI Components
//

// Header
isc.HLayout.create({
ID: "newsreaderHead",
width: "100%",
height: 50,
backgroundColor: "navy",
autoDraw: false,
members: [
isc.Label.create({
contents: "Wired",
align: "left",
overflow: "hidden",
width: "50%",
autoDraw: false,
className: "readerHeaderText"
}),
isc.Label.create({
contents: "News Reader",
align: "right",
overflow: "hidden",
width: "50%",
autoDraw: false,
className: "readerHeaderText"
})
]
});

// Article list
isc.ListGrid.create({
ID: "articleList",
width: "100%",
dataSource:"newsFeed",
autoFetchData: true,
autoDraw: false,
fields: [
{ name: "title" },
{ name: "pubDate", title: "Publish Date", width: 200 },
{ name: "link" }
],
recordClick : function(viewer, record, recordNum, field, fieldNum, value, rawValue) {
isc.logEcho(record, "recordClick: ");
articlePane.setContentsURL(record.link, "page");
},
canHover: true,
cellHoverHTML : function(record, rowNum, colNum) {
isc.logWarn("hover: " + record.description);
return record.description;
},
dataArrived : 'if (startRow == 0) { articlePane.setContentsURL(articleList.getRecord(0).link, "page");}',
height: "30%",
showResizeBar:true
});

// Article view
isc.HTMLPane.create({
ID:"articlePane",
width: "100%",
showEdges:false,
contentsType:"page",
autoDraw: false
});

// UI Layout
isc.VLayout.create({
width:"100%",
height:"100%",
members:[
"newsreaderHead",
"articleList",
"articlePane"
]
});

</script>
</body>
</html>

Friday, April 17, 2009

Movement again

OK. It's been almost 3 months since I have posted anything here so I wanted to catch you up. At the end of January a colleague at my contracted died in a car crash and the resulting load on me has increased dramatically. Couple that with a cutback on most new development (i.e. SmartClient-based prototyping) due to the economic slowdown and there hasn't been much to report.

There is some good news, though. I have integrated the in-field hints, keyPress filter and character casing options I published earlier into the official 7.0 sources as standard features. I hope to do the same with the masked edit feature.

If you have used any of my published code or ideas, I would love to hear from you on you experience and what parts you have found most useful. Any suggestions for improvements or fixes would also be appreciated.

Saturday, January 17, 2009

Field dependencies

A form will typically have a number of fields that are dependent on conditions with another field. Consider a selection that is required if another field is used. Or maybe a field is disabled if a certain combo value is chosen. No matter, these dependencies have to be managed somewhere. In traditional client development this is likely to be handled by a controller (i.e. MVC) but there are times the dependencies are not complex and a full blown controller is just not necessary.

Take the example I posted in my previous blog entry shown again below.

fields: [
{ name:" major="" comboboxitem="" select="">
width: "*",
optionDataSource: "majorValuesDS",
allowEmptyValue: true,
valueField: "major",
displayField: "description",
completeOnTab: true,
changed: function(form, item, value) {
var field = form.getField('minor');
field.setValue(null);
field.setDisabled(!value);
},
pickListWidth: 450,
pickListFields: [
{ name: "major", width: 50 },
{ name: "description" }
]
},
{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
width: "*",
optionDataSource: "minorValuesDS",
autoFetchData: false,
allowEmptyValue: true,
disabled: true,
valueField: "minor",
displayField: "description",
completeOnTab: true,
pickListWidth: 450,
pickListFields: [
{ name: "minor", width: 50 },
{ name: "description" }
],
getPickListFilterCriteria: function() {
return {
major: this.form.getValue("major"),
minor: this.form.getValue("minor")
}
}
}
]

Notice that when the major is changed the minor is automatically cleared and is enabled only if the major actually has a value. All of this is handled in the changed handler of the major. Finally, note also that the minor puts the pick list criteria together using the value from major. So the dependency is dealt with by both fields. I would rather see all of this in one place: the minor.

So, how can we accomplish this? We can use an observer. The minor field just needs to register an observer on the parent field (major) changed event. The observer can then be written as:

{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
...
majorChanged: function(item, majorItem) {
// Major changed - clear our selection, if any
item.setValue(null);
// Disable this field if major is empty
var major = majorItem.getValue();
item.setDisabled(!major);
},
...

That looks nice. Now when majorChanged is called the minor field can update itself as needed. But how is this method called? We need to register an observer on the major field changed event handler. Something like:

{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
...
init: function() {
this.Super("init", arguments);
// Register observer on major
var field = this.getField('major');
this.observe(field, "changed", "observer.itemIdChanged(observer, observed)");
},
...

That's easy enough! But, wait! It doesn't work. Apparently it's too early at init() time to register an observer on another field. So we need a later event to make this observation. Once the fields have been defined on the form all fields are initialized with a call to setValue(null) by the form. So we can use this event to register our observer.

Final working code ends up as:

{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
...
setValue: function(newValue) {
this.Super("setValue", arguments);
// Register observer on major
if (!this._observing) {
var field = this.getField('major');
this.observe(field, "changed", "observer.majorChanged(observer, observed)");
this._observing = true;
}
},
majorChanged: function(item, majorItem) {
// Major changed - clear our selection, if any
item.setValue(null);
// Disable this field if major is empty
var major = majorItem.getValue();
item.setDisabled(!major);
},
...

Now, after removing the major's changed event handler only the minor field knows about its dependency on the contents of major.

I don't particularly like using setValue in this way but I haven't found a better solution. If you have a suggestion, please let me know. Otherwise, happy SmartClienting.

Thursday, January 15, 2009

Dependent ComboBoxes

The application which is the target of my prototyping with SmartClient uses a number of dependent combo boxes to enter information or search criteria. In some cases the selection list is short and static so value maps can be used. Other times the list is larger and it makes more sense to pull the values from a dataSource using the optionDataSource configuration. Setting these dependencies up is very straigtforward and one of the excellent features of SmartClient.

Here is an example of two combo box fields in a form:

fields: [
{ name:" major="" comboboxitem="" select="">
width: "*",
optionDataSource: "majorValuesDS",
allowEmptyValue: true,
valueField: "major",
displayField: "description",
completeOnTab: true,
changed: function(form, item, value) {
var field = form.getField('minor');
field.setValue(null);
field.setDisabled(!value);
},
pickListWidth: 450,
pickListFields: [
{ name: "major", width: 50 },
{ name: "description" }
]
},
{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
width: "*",
optionDataSource: "minorValuesDS",
autoFetchData: false,
allowEmptyValue: true,
disabled: true,
valueField: "minor",
displayField: "description",
completeOnTab: true,
pickListWidth: 450,
pickListFields: [
{ name: "minor", width: 50 },
{ name: "description" }
],
getPickListFilterCriteria: function() {
return {
major: this.form.getValue("major"),
minor: this.form.getValue("minor")
}
}
}
]

In this example there is one combo to select a major and one to select a minor. Minor values are unique to the major so fetching the available minors requires the major to be part of the criteria. The way that is handled is overriding the getPickListFilterCriteria() on the minor field. Note also in the example that when the major changes, the minor is cleared and then enabled if there is a valid major.

The above example works great for data entry. When a value is selected in either combo, the description is shown as expected. However, what if we want to edit an existing entity that has major and minor values and we expect the description to be shown at load time?

editItem: function(form) {
form.fetchData();
var field = form.getField('minor');
field.setDisabled(false);
}

Above is an example fetch on the form for an exiting item. Criteria would normally be included in the fetchData() call but is excluded here for clarity. The same can be accomplished with an editRecord() call also.

Well, this example doesn't work so well in our case because the minor is not the primary key for our dependent combo. Unfortunately, as I posted on the SmartClient forums it turns out that upon setting the initial value for minor does trigger a fetch on the optionDataSource but getPickListFilterCriteria() is not called so the search criteria is wrong. Thus the description is not found to be displayed.

A workaround that I have previously used successfully is to add a call to minor.fetchData() after loading the record which forces the combo to refresh its list. At that time it does use our dynamic criteria and the result is correct.

This has been bothering me for a while so I finally dug into the code to find a fix for this situation. A patch for the FormItem base class is included below that fixes the issue. I cannot vouch for this patch not having side-effects so I would be interested to know if anyone does find issue with it.

isc.FormItem.addMethods({

//_checkForDisplayFieldValue : function (newValue) {
$43f: function(newValue) {
//var inValueMap = (this._mapKey(newValue, true) != null);
var inValueMap = (this.$17b(newValue, true) != null);

if (!inValueMap) {
var ods = this.getOptionDataSource();
if (ods) {
// Check for the case where we're already fetching the value
var recordCrit = {};
if (!this.filterLocally) {
if (this.getPickListFilterCriteria)
recordCrit = this.getPickListFilterCriteria();
else
recordCrit[this.getValueFieldName()] = newValue;
}
ods.fetchData(recordCrit,
{ target: this,
methodName: "fetchMissingValueReply"
},
{ showPrompt: false, clientContext: { dataValue: newValue} }
);
}
}
}

});

This change is to an internal method called FormItem._checkForDisplayFieldValue so I had to use it obfuscated name $43f instead. Normally these internal methods are not meant to be replaced so they are therefore obfuscated as part of the minimization process. The contents of the patch above are from the distributed source (with the _mapKey obfuscated method replaced as well).

The change I made is to call getPickListFilterCriteria() if it is defined on the derived form item (ex. ComboBoxItem) instead of creating the criteria solely from the field's value.

Now, as suggested by the editItem function above, if there was only an enableIf() method on form items...

If this fix is of value to you, please let me know your results.

Tuesday, January 13, 2009

Extended TextItem

Along with the MaskedTextItem I detailed in a previous blog entry the desire for an enhanced text field to support automatic case conversion on the fly and keystroke filtering. Well I finally broke down and wrote one detail here.

The new ExtTextItem can be used in place of the TextItem and it will function identically if not configured for the enhanced features. To enable automatic case conversion, set the characterCasing property to either "Upper" or "Lower". All typed alphabetic characters will then be translated as desired. Additionally, any value placed into the field via setValue() will also be translated.

Keypress filtering can also be used to limit entry in the field to just the desired characters. This serves as a preemptive form of validation. Assign a string representing a regular expression to keyPressFilter that defines the characters desired in the field. All others are rejected. For example, a filter of "[0-9]" would only allow numeric entry.

Finally as prompted by a recent Ajaxian posting, ExtTextItem also includes support for embedding the hint within the field instead of trailing. This is made possible by using a new CSS style textItemHint and is enabled by setting the hintInField property to true. Here is an example of the embedded hints:

When the field receives focus the hint is hidden. On loss of focus the hint is shown if the value is still empty.

The source for the control can be downloaded from ExtTextItem.js.