Quick Edit mode for YUI 2 DataTable

August 19, 2010 at 8:42 am by John Lindal | In Development | 5 Comments

YUI 2 DataTable provides slick inline editing. When disableBtns is turned on in the column configuration, editing simple values like strings or numbers feels just like Excel. However, the experience cannot be as responsive as a desktop application because each change typically requires an XHR call to the server to store (or reject!) the new value. If the user needs to change many of the displayed values, it can be a slow and frustrating experience. To solve this, I developed QuickEditDataTable. This extends DataTable to add Quick Edit mode, which allows all editable values to be changed in one bulk operation:

(Click the screenshot to play with this example.)

Overview

To enter Quick Edit mode, call startQuickEdit(). To exit Quick Edit mode, call cancelQuickEdit().

It is your responsibility to save the changes before calling cancelQuickEdit(). To simplify this task, QuickEditDataTable provides getQuickEditChanges(). This returns an array of objects, one for each row. Each object contains only the values that were changed in that row, keyed off the column id’s. For example, if the table has 4 columns (title, author, year, quantity), and the user only changed the quantity in one row to 20, then the object for that row would be {quantity:20}. The other values would be omitted.

QuickEditDataTable can easily extend YAHOO.widget.ScrollingDataTable if you need that functionality. If you need this, simply make a copy of the source file and change the base class.

Configuration

Quick Edit is a modal state in which the cell formatters for editable columns are swapped out and replaced with special formatters that generate input, textarea, or select elements. Only columns that have quickEdit configuration will be editable. The configuration options are:

copyDown

If true, the top cell in the column will have a button to copy the value down to the rest of the rows.

formatter

The cell formatter which will render an appropriate form field: <input type=”text”>, <textarea>, or <select>. By default, the cell formatter YAHOO.widget.QuickEditDataTable.textQuickEditFormatter is used for all cells to produce input elements. To get a textarea element, configure a column to use YAHOO.widget.QuickEditDataTable.textareaQuickEditFormatter instead. You can also write a custom quick edit formatter — see below.

validation

Validation configuration for every field in the column.

css

CSS classes encoding basic validation rules:

yiv-required

Value must not be empty.

yiv-length:[x,y]

String must be at least x characters and at most y characters. At least one of x and y must be specified.

yiv-integer:[x,y]

The integer value must be at least x and at most y. x and y are both optional.

yiv-decimal:[x,y]

The decimal value must be at least x and at most y. Exponents are not allowed. x and y are both optional.

fn

A function that will be called with the DataTable as its scope and the cell’s form element as the argument. Return true if the value is valid. Otherwise, call this.displayQuickEditMessage(...) to display an error and then return false.

msg

A map of types to messages that will be displayed when a basic or regex validation rule fails. The valid types are: required, min_length, max_length, integer, decimal, and regex. There is no default for type regex, so you must specify a message if you configure a regex validation. The default error messages for the other types are stored in YAHOO.widget.QuickEditDataTable.Strings and can be overridden and/or localized.

regex

Regular expression that the value must satisfy in order to be considered valid.

Sometimes, a non-editable column must be rendered differently during Quick Edit mode. The best example is a column containing a link, since navigating away from the page while in Quick Edit mode can be disastrous. To remove the link during Quick Edit, configure qeFormatter for the column to be YAHOO.widget.QuickEditDataTable.readonlyLinkQuickEditFormatter. For email addresses, use YAHOO.widget.QuickEditDataTable.readonlyEmailQuickEditFormatter. You can also write you own custom, read-only formatter. Simply follow the normal rules for constructing a DataTable cell formatter.

Custom Quick Edit Formatters

To write a custom cell formatter for QuickEdit mode, you must structure the function as follows:

function myQuickEditFormatter(el, oRecord, oColumn, oData) {
  var markup =
    '<input type="text" class="{yiv} yui-quick-edit yui-quick-edit-key:{key}"/>' +
    YAHOO.widget.QuickEditDataTable.MARKUP_QE_ERROR_DISPLAY;
    el.innerHTML = lang.substitute(markup, {
      key: oColumn.key,
      yiv: oColumn.quickEdit.validation ? (oColumn.quickEdit.validation.css || '') : ''
    });
    el.firstChild.value = extractMyEditableValue(oData);
    YAHOO.widget.QuickEditDataTable.copyDownFormatter.apply(this, arguments);
};

You can use textarea or select instead of input, but you can only create a single field.

extractMyEditableValue() does not have to be a separate function nor must it be limited to using only oData. The work should normally be done inline in the formatter function, but the name of the sample function makes the point clear.

About the author: John Lindal (@jafl5272 on Twitter) is one of the lead engineers constructing the foundation on which Yahoo! APT is built. Previously, he worked on the Yahoo! Publisher Network.

Share and extend: Bookmark with del.icio.us | digg it! | reddit!

Treeble: Using Nested YUI 2 DataSources for Row Expansion

April 14, 2010 at 8:00 am by John Lindal | In Development | 29 Comments

John Lindal (@jafl5272 on twitter) is one of the lead engineers constructing the foundation on which Yahoo! APT is built. Previously, he worked on the Yahoo! Publisher Network.

Daniel Barreiro’s recent post about nested tables reminded me that it was about time I finished my “treeble” widget. “Treeble” comes from merging “tree” and “table.” The original motivation was to enable drilling into the details behind each row in a table, e.g., start with a table which displays sales figures for each continent and then drill into each country, region, and city. Of course, once I started building it, the natural design was to support any hierarchy of data. As an example, it can display a tree like this:

(Click the screenshot to play with this example.)

Overview

The treeble widget consists of two parts. The first is a small extension to YUI DataTable for opening/closing nodes in the tree. (This is done by augmenting DataTable in order to allow it to work transparently with existing extensions to DataTable, most notably ScrollingDataTable.) The second part of treeble is a significant extension to YUI DataSource, called TreebleDataSource, which merges the results from simple, flat data sources. The tree is built dynamically, so you don’t have to load all your data at once.

The tricky part is paginating the tree: knowing the subset of nodes which have been opened, request only the visible items. TreebleDataSource provides two options:

  1. Paginate top-level nodes only, so all their children can be visible on the same page (the default)
  2. Paginate so a fixed number of nodes are visible

#1 is ideal when the tree is shallow and there are only a few children per node. #2 is necessary when there may be a very large number of children per node. The configuration option that controls this behavior is paginateChildren. In order for pagination to work, the total number of items available from each simple data source must not change.

Since all the items from all the simple data sources are displayed in a single table with a single set of columns, the schema given to DataTable must be the union of all the flat data source schemas. For example, in the above screenshot, DataTable must have a column for Quantity even though it is only defined for the leaf nodes.

Obviously, rows in the table no longer map directly to records in the simple data sources. Instead, each record in the DataTable has three special members:

_yui_node_ds

The DataSource from which the record was retrieved. If you allow inline editing, this tells you which DataSource to use when saving the new value. (You can edit the Quantity column in the live example.)

_yui_node_depth

The depth of the node in the tree. Top-level nodes have depth zero. This is useful when indenting child nodes, as in the above screenshot.

_yui_node_path

Array of node indices leading to the record. For example, [2,5,1] translates to second child of the sixth child of the third top-level node. DataTable.rowIsOpen() and DataTable.toggleRow() require this array to identify the node.

The YUIDoc for TreebleDataSource and the extensions to DataTable is here.

Configuring DataTable

To work with TreebleDataSource, your YUI DataTable must be configured correctly:

var myDataSource = new YAHOO.util.TreebleDataSource(...);
var myDataTable = new YAHOO.widget.DataTable(container, columns, myDataSource,
{
  dynamicData:true,
  generateRequest: YAHOO.widget.DataTable.generateTreebleDataSourceRequest,
  displayAllRecords: the opposite of your TreebleDataSource's paginateChildren value,
  other configuration, e.g., paginator
});

The special version of generateRequest is required because TreebleDataSource needs to receive a known data format so it can correctly generate the requests to the individual simple data sources.

Constructing TreebleDataSource

When you construct TreebleDataSource, you must pass it an instance of YUI DataSource as oLiveData. One column in the data schema for this data source must have its parser set to either "datasource" or a custom parsing function which does the same job: construct a new YUI DataSource from the value in the column. The default data source parser ignores falsey values and constructs a data source from any truthy value, e.g., an array of objects, an XHR URL, or an object which defines dataType and liveData. The configuration (including responseSchema and treebleConfig) for the new data source is copied from the parent data source. Writing a custom datasource parser is discussed later in this post.

TreebleDataSource itself has only one configuration parameter, paginateChildren, which controls the pagination behavior, as discussed earlier. The majority of the treeble-related configuration is set on the simple data sources via the treebleConfig object. This allows the configuration to be different for each simple data source, as discussed below in the section on mixed data sources.

Using local DataSources

The live example demonstrates how to work with local DataSources. The only required values in treebleConfig are generateRequest and totalRecordsReturnExpr:

new LocalDataSource(array,
{
  ...,
  treebleConfig:
  {
    totalRecordsReturnExpr: '.meta.totalRecords',
    generateRequest: function(state, path)
    {
      return state;
    }
  }
});

Since LocalDataSource ignores all request parameters, generateRequest could actually return null, but it echoes the state object instead to support extensions which can sort the data.

Setting totalRecordsReturnExpr signals TreebleDataSource that the simple data source will return all its records, not just the requested slice. The actual value of totalRecordsReturnExpr must be an OGNL expression specifying where in the oParsedResponse object returned by TreebleDataSource to store the total number of visible nodes, based on which nodes are currently open. In the example, this is oParsedResponse.meta.totalRecords. Since DataTable has (and must have!) dynamicData set to true, the live example also overrides DataTable.handleDataReturnPayload to set oPayload.totalRecords=oParsedResponse.meta.totalRecords. This gives the paginator the information it needs to compute the total number of pages.

Using XHR DataSources

An example configuration when using XHR DataSources would be to construct the top-level data source as:

new XHRDataSource(url,
{
  ...,
  treebleConfig:
  {
    startIndexExpr: '.meta.startIndex',
    totalRecordsExpr: '.meta.totalRecords',
    generateRequest: function(state, path)
    {
      return 'path='+path.join(',')+
                 '&startIndex='+state.startIndex+
                 '&results='+state.results+
                 '&sort='+state.sort+
                 '&dir='+state.dir;
    }
  }
});

In this example, generateRequest returns query args which the server will interpret in order to return the appropriately sorted slice of the children of the node specified by path.

The value of startIndexExpr is an OGNL expression specifying where in the oParsedResponse object returned by the simple data source the index of the first returned node is stored. In the example, this is oParsedResponse.meta.startIndex.

The value of totalRecordsExpr is an OGNL expression specifying where in oParsedResponse the total number of nodes is stored. In the example, this is oParsedResponse.meta.totalRecords. totalRecordsExpr also specifies where in the oParsedResponse object returned by TreebleDataSource to store the total number of visible nodes, based on which nodes are currently open.

Since DataTable has (and must have!) dynamicData set to true, you would also have to override DataTable.handleDataReturnPayload to set oPayload.totalRecords=oParsedResponse.meta.totalRecords. This gives the paginator the information it needs to compute the total number of pages.

Using Mixed DataSources

TreebleDataSource does not require that all the simple data sources be the same type. For example, if you have a large number of top-level nodes, but only a small tree of children for each node, then it makes sense to return the entire tree when a top-level node is opened. The default data source parser actually handles this automagically if you specify startIndexExpr, totalRecordsExpr, and totalRecordsReturnExpr for the top-level data source!

If you have only a few top-level nodes, but each tree of children is huge, then your top-level data source could use local data, and you could build a custom data source parser which instantiates XHR data sources for the children, setting startIndexExpr, totalRecordsExpr, and generateRequest appropriately.

You are only limited by your ability to comprehend the complexity of the system!

Note that, when using a custom data source parser, you must define childNodesKey in treebleConfig for each simple data source so TreebleDataSource knows the name of the data source column. (When you use the default parser, this is detected automatically.)

Share and extend: Bookmark with del.icio.us | digg it! | reddit!

In the YUI 3 Gallery: John Lindal’s Form Manager

March 23, 2010 at 11:08 am by John Lindal | In Development, YUI 3 Gallery | 3 Comments

John Lindal (@jafl5272 on twitter) is one of the lead engineers constructing the foundation on which Yahoo! APT is built. Previously, he worked on the Yahoo! Publisher Network.

The first version of the code discussed in this article was written in 2006. It has continued to evolve ever since, and now it’s been shared as part of the YUI 3 Gallery as the Form Manager module.

Forms have been a staple on web sites for a very long time. In the early days, they were quite simple: the user entered values and then waited while the server processed the values or spit back errors. The rise of Web 2.0 has significantly improved the experience, most notably by pre-validating on the client. This provides immediate feedback and avoids pointless connections to the server.

Pre-validation is merely one aspect of forms, however. The entire cycle is:

  1. pre-populate the form with default values;
  2. pre-validate the input in the browser;
  3. submit the form data to the server synchronously or asynchronously;
  4. display the results returned by the server (success or errors).

When combined with YUI 3 IO, the YUI 3 Gallery module Form Manager supports this full cycle. You can play with the client-side functionality here.

Initialization

The first step, pre-populating the form with default values, is of course best done by setting values directly in the markup, because this works even when JavaScript is turned off. However, you can also pass a map of default values, keyed on the names of the form elements, to the Form Manager constructor. When you call prepareForm(), it merges the default values from the DOM with the default values passed to the constructor, with the constructor values taking precedence. The result is saved so you can easily reset to these values by calling populateForm(). You can also modify these stored defaults: setDefaultValues(), setDefaultValue(), or saveCurrentValuesAsDefault(). (Note that this is different from the browser’s native reset function, since that uses only values encoded in the DOM. Form Manager provides clearForm() as a wrapper for reset.)

Another useful function to call during initialization is initFocus(). This sets focus to the first element in the form. If filling out the form is the main reason for visiting the page, this saves the user a click. Obviously, if you have more than one form on the page, you should only call initFocus() for one of them.

Pre-validation

Pre-validating user input is a tricky business. In my experience, the simplest approach is best: check everything after the user says I’m done. This avoids the need to filter the input stream (keyup is easy to catch, but paste is notoriously difficult, and it all leads to very unexpected edge case behaviors) and, more importantly, it doesn’t interrupt the user’s flow. This is why Form Manager provides a single function to validate everything in the form (drum roll, please): validateForm().

Unlike other solutions, Form Manager stores most of the validation configuration in the DOM. To mark a field for validation, apply one or more of the following CSS classes directly to the field:

yiv-required

Value must not be empty.

yiv-length:[x,y]

String must be at least x characters and at most y characters. At least one of x and y must be specified.

yiv-integer:[x,y]

The value must be an integer, and the value must be at least x and at most y. x and y are both optional.

yiv-decimal:[x,y]

The value must be a decimal, and the value must be at least x and at most y. Exponents are not allowed. x and y are both optional.

For example, if a field must be filled in, and it only accepts between 6 and 10 characters, the CSS class would be yiv-required yiv-length:[6,10].

One nice benefit of encoding validation in CSS classes is that it can be applied in related situations, e.g., when editing dynamically created fields in a table. (I hope to post an example for YUI 2 DataTable soon.) FormManager exposes the static function validateFromCSSData() so you don’t have to reinvent the wheel.

If you need to use a regular expression to validate a field, register it by calling setRegex(). For anything else, you can attach a function to a field by calling setFunction(). If you need to perform checks that encompass multiple fields, you can override postValidateForm() on your instance of Y.FormManager.

One final note: As the name suggests, pre-validation is not real validation. JavaScript is relatively easy to subvert (or turn off completely), so the server must never trust anything that it receives from the client. In addition, some checks can only be done on the server, e.g., anything that requires hitting the database.

Displaying Errors

Obviously, if either pre-validation on the client or validation on the server fails, then you need to notify the user, ideally by highlighting the fields that need attention. Form Manager supports this via the function displayMessage().

This function expects certain CSS marker classes on the DOM surrouning each form element or tightly coupled set of form elements. My favorite layout is:

<div class="formmgr-row>
  ...element label...
  <span class="formmgr-message-text"></span>
  ...form element marked with CSS class formmgr-field...
</div>

This localizes well, since the label is above the field, and when an error message is displayed, it’s very clear to which field the error applies. To see it in action, follow this link and click the Validate button in the upper left corner of the page.

But that is just my preference. You can arrange the DOM elements any way you want inside the container marked by formmgr-row, as long as somewhere inside is another container marked by formmgr-message-text, and the field itself is marked by formmgr-field.

Message Types

One important point is that displayMessage() requires a message type. The supported types are stored in Y.FormManager.status_order in order of precedence. The default is [ 'error', 'warn', 'success', 'info' ], but you can modify this to suit your needs. The ordering is important because, if you call displayMessage() with a higher precedence type and the field is already displaying a message with a lower precedence, then the new message will replace the original message. Similarly, a lower precedence message will be ignored if a higher precedence message is already displayed. This allows you to toss messages at each field with abandon, secure in the knowledge that errors will override warnings.

When a message is displayed, the container marked with formmgr-row and the field marked with formmgr-field both get an extra CSS class: formmgr-hastype, where type is the message type. This allows you to style the message, field, label, etc. in a different way for each message type. In addition, the fieldset containing the form field also gets the same class. This can be used to direct the user’s attention when the form is large. (If several fields within a fieldset have different types of messages, the highest precedence type is set on the fieldset.)

Messages

Form Manager includes a default set of error messages for all the validations that can be encoded in CSS. These strings are stored in Y.FormManager.Strings, so you can modify and/or localize them.

You can also specify custom messages for individual fields by calling setErrorMessages().

Note that there is no default message for a regular expression validator, because anything generic like The value does not match the required pattern. is utterly meaningless to the user. If you do not set a message for the type regex before setting the regular expression itself, Form Manager will log an error to remind you.

Submitting the Form

Regardless of whether you submit the data synchronously (via form.submit()) or asynchronously (via Y.io), you will probably want to disable the form while the data is being processed. Form Manager automatically finds all buttons inside the <form> element. If you have additional buttons elsewhere on the page, you can register them by calling registerButton(). All known buttons will be disabled when you call disableForm(). (If you use XHR, call enableForm() after you receive the response from the server!)

If you submit the form synchronously, then you will serve the same page again if there are errors. In order to work without JavaScript, you should write the errors directly into the DOM, the same way that Form Manager does it.

If you submit via XHR, then you know that JavaScript is enabled, so you can use displayMessage() to highlight the values which the server rejected. Obviously, this requires that the response from the server include detailed error information!

As a final note, if the form is in an overlay, then you should only close the overlay if the server response with success; i.e., display errors in the overlay, but display a success message somewhere prominent on the main page.

Share and extend: Bookmark with del.icio.us | digg it! | reddit!

In the YUI 3 Gallery: Checkbox Group Behaviors

March 1, 2010 at 1:31 pm by John Lindal | In Development, YUI 3 Gallery | No Comments

John Lindal (@jafl5272 on twitter) is one of the lead engineers working on the foundation on which Yahoo! APT is built. Previously, he worked on the Yahoo! Publisher Network.

Checkboxes and radio buttons are well known patterns for choosing from a small set of items. The former lets you choose any subset of items (including none), while the latter requires exactly one selection.

But what if you need a different behavior? The Checkbox Groups module in the YUI 3 Gallery implements three common cases and a foundation for constructing others. The module is based on checkboxes because, by default, they do not enforce any restrictions, which makes them an ideal foundation on which to build.

The first behavior provided by the module is Y.AtLeastOneCheckboxGroup. This enforces that at least one item must be selected. More than one selection is permitted, but deselecting all items is prevented. This implemented using the “drop of mercury” algorithm discussed in Tog on Interface: Whenever you try to deselect the last item, the selection slides out from under the cursor. You can play with it here.

The second example (Y.AtMostOneCheckboxGroup) allows no selection, but more than one selection is not permitted. Note that you cannot use radio buttons for this, because then it is not possible to unselect the current item. This is demonstrated in the second example on this page.

The final example (Y.SelectAllCheckboxGroup) implements a “select all” behavior using an extra checkbox. Selecting the “Select All” checkbox selects all the other items. Deselecting it deselects all other items. Selecting or deselecting any item updates the state of the “Select All” checkbox. You can try it out by playing with the third example on this page.

The possiblities are endless. You can build your own custom behavior quickly by extending the base class used by all the above examples: Y.CheckboxGroup. This takes care of all the bureaucracy, so all you have to do is implement enforceConstraints(). The function is invoked with the list of managed checkboxes and the index of the checkbox that has just been changed. You can then inspect and update the state of all the checkboxes to enforce your custom constraints.

In many cases, all you need are the checkboxes themselves, e.g., Y.AtLeastOneCheckboxGroup and Y.AtMostOneCheckboxGroup. For this, your constructor can be pass-through, since the base class Y.CheckboxGroup will manage the list for you. If you need additional controls, e.g., Y.SelectAllCheckboxGroup, your constructor should require references to these controls, and you will need to store them so you can access their state inside enforceConstraints().

To use the Checkbox Groups module, include the following script on your page:

<script type="text/javascript" src="http://yui.yahooapis.com/combo?3.0.0/build/yui/yui-min.js&gallery-2009.12.08-22/build/gallery-checkboxgroups/gallery-checkboxgroups-min.js"></script>

The provided behaviors are all install-and-forget:

YUI().use('gallery-checkboxgroups', function(Y)
{
	// attaches behavior to all checkboxes with CSS class "my-at-least-one-checkbox-group"
	new Y.AtLeastOneCheckboxGroup('.my-at-least-one-checkbox-group');

	// attaches behavior to all checkboxes with CSS class "my-at-most-one-checkbox-group"
	new Y.AtMostOneCheckboxGroup('.my-at-most-one-checkbox-group');

	// attaches behavior to all checkboxes with CSS class "my-select-all-checkbox-group",
	// controlled by the checkbox with id "my-select-all-checkbox"
	new Y.SelectAllCheckboxGroup('#my-select-all-checkbox', '.my-select-all-checkbox-group');
});

One final note: Ideally, checkboxes with custom behavior should be styled differently, so the user has some indication that they are not just plain checkboxes. For example, Tog suggests using diamonds for Y.AtLeastOneCheckboxGroup. In practice, however, you must also ensure that people can figure out that your controls are to be used for selecting items. So be clever, just not too clever!

Share and extend: Bookmark with del.icio.us | digg it! | reddit!

Hosted by Yahoo!

Copyright © 2006-2010 Yahoo! Inc. All rights reserved. Privacy Policy - Terms of Service

Powered by WordPress on Yahoo! Web Hosting.