• Home
  • Quick Start
    • Configurator
    • Download YUI 3
  • Documentation
    • User Guides
    • Examples
    • Tutorials
    • API Docs
  • Community
    • Gallery
    • Blog
    • Forums
    • YUI Theater
    • Calendar
  • Contribute
    • YUI on GitHub »
    • File a Ticket
    • View Tickets
    • Dashboard
  • Other Projects
    • YUI 2
    • YUI Compressor
    • YUI Doc »
    • YUI Builder
    • YUI PHP Loader
    • YUI Test
    • YUI Website
  • YUI
  • Blog
  • YUI 3 Gallery

Blog: Category ‘YUI 3 Gallery’

« Older Entries

In the YUI 3 Gallery: Bulk Editor Widget

The QuickEdit plugin for YUI 3 DataTable makes it easy to edit an entire page of records as an atomic operation. However, sometimes you need to do even more. For example, you might have to simultaneously edit more records than you can comfortably fit on a single page. Or you might need to support adding, duplicating, and removing records as part of the atomic operation. Or you might wish to visually group fields by placing them in a single table cell. The Bulk Editor widget supports all these possibilities.

(Click the screenshot to play with this example.)

Overview

The Bulk Editor widget consists of three components:

Data source

This wraps a YUI DataSource and manages the changes: insertions, removals, and changed values.

Base widget

This provides the basic structure for editing by managing records and fields within each record. Derived classes are responsible for rendering each record into a separate row, which could be a div, a tbody, or some other container.

HTML table implementation

This extends the base widget to render each record into a tbody in an HTML table. The column configuration determines which field is displayed in each column of the table. A custom cell formatter can be used to render multiple fields in a single table cell.

Configuration

In the example that generated the above screenshot, the configuration has been kept as simple as possible:

fields defines the editable values in each record. The default type is input. The other valid types are select and textarea. (select requires a list of values.) Basic validation is provided by Form Manager gallery module. This covers required fields, length restrictions, and numeric ranges. More complex validation can be performed by specifying regex or your own function (fn). Here is an excerpt from the live example:

var fields =
{
	title:
	{
		type: 'textarea'
	},
	year:
	{
		validation:
		{
			css: 'yiv-integer:[1500,2100]'
		}
	},
	color:
	{
		type: 'select',
		values:
		[
			{ value: 'red',   text: 'Red'   },
			{ value: 'green', text: 'Green' },
			{ value: 'blue',  text: 'Blue'  }
		]
	}
};

Y.BulkEditDataSource requires an instance of Y.DataSource and the following parameters:

uniqueIdKey

The name of a key which uniquely identifies each record.

generateRequest

A function to generate request parameters for Y.DataSource. (This is empty in the example, because Y.DataSource.Local always returns all the data.)

extractTotalRecords

A function to extract the total number of records from the Y.DataSource response.

Since the example uses Y.DataSource.Local, totalRecordsReturnExpr is also required. This OGNL expression specifies where in the response to store the total number of records. (Notice that extractTotalRecords reads this value.)

var ds = new Y.BulkEditDataSource(
{
	ds:                     raw_ds,
	uniqueIdKey:            'id',
	generateRequest:        function() { },
	totalRecordsReturnExpr: '.meta.totalRecords',
	extractTotalRecords:    function(response)
	{
		return response.meta.totalRecords;
	}
});

Y.HTMLTableBulkEditor requires the data source, the field configuration, and the column configuration. In the column configuration, the key is the field name, unless you specify a custom formatter. The label is used as the column title. Here is an excerpt from the live example:

var columns =
[
	{
		key:       'checkbox',
		label:     '<input type="checkbox" id="select-all" />',
		formatter: function(o)
		{
			var markup = '<input type="checkbox" class="record-select" id="{id}" />';
			o.cell.set('innerHTML', Y.Lang.sub(markup,
			{
				id: this.getRecordId(o.record)
			}));
		}
	},
	{ key: 'title', label: 'Title' },
	{ key: 'year', label: 'Year' },
	{ key: 'color', label: 'Color' }
];

(Note that the live example defines a minor extension to Y.HTMLTableBulkEditor to handle the checkbox column.)

You can also pass an instance of Y.Paginator to Y.BulkEditDataSource. This is demonstrated in a separate, more complicated live example.

Local vs. Remote Data Sources

When deciding whether to use a local or a remote datasource, you must carefully consider the trade-offs. The obvious trade-off is that a local datasource is faster when paginating, but the initial page load will take longer, and it requires more memory on the client.

The Bulk Editor widget imposes additional trade-offs, however.

First, the YUI DataSource must return immutable data. This is automatic for local data sources, but can be tricky to implement for remote data sources. You will need to lock the rows in your database table for the duration of the bulk edit operation if more than one user is allowed to modify them.

Second, the choice between local and remote data source affects how you are allowed to save the data. When you use a local data source, you can do best effort saving, i.e., save all the valid records to the server, remove them from the local datasource, and allow the user to focus on the records that have invalid values. When you use a remote data source, the immutability requirement only allows you to do all or nothing saving, i.e., the data can only be saved after all the data is valid.

Real-world Use Case

The original motivation for the Bulk Editor widget was to allow post-processing of an uploaded spreadsheet. Introducing a post-processing step removes the need for the spreadsheet values to be perfect. Errors can be flagged and fixed in the Bulk Editor widget instead of rejecting the entire upload. In addition, processing on the server can do best-guess assignment of additional values required for each record, and the user can check and fix these extra values before saving. This simplifies the initial creation of the spreadsheet.

In this scenario, a remote data source is the best choice. The uploaded data is stored in a scratch space, and is therefore guaranteed immutable, since no other user can see it. “All or nothing” saving is appropriate: Once all the errors have been fixed, the save operation is atomic, just like a standard upload operation.

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.

By John LindalDecember 5th, 2011

Updated: The “MakeNode” Widget Extension

Editor’s Note: This article was originally published earlier this year. Since then, the MakeNode module has been published to the YUI Gallery and received some enhancements. Today’s article reflects all the latest changes to MakeNode.

In my previous article, A
Recipe for a YUI 3 Application
, I showed a way to use
Y.substitute as a very basic template
processor. The idea took life from there, with suggestions from the
folks in the #yui IRC channel, and I made it a Widget extension that
is available on the YUI Gallery, called MakeNode.
MakeNode is not a generic template processor and it is not meant as
one. On the other hand, it is tightly integrated with the YUI Widget

foundation class, including className and event helpers and
internationalization. In this article, I will take the Spinner
example and modify it to follow the guidelines from my previous
article and to use MakeNode. MakeNode
is available as a gallery component as well as the modified Spinner
component and the example
that will be used in this article.


Extending your component


To load MakeNode you need to include the module in your

YUI().use() statement using the name
‘gallery-makenode’or, if defining a
module via YUI.add(), list it as the
requires array. Then, to extend your
widget, you list it in the third argument to Y.Base.create(),
like this:


Y.Spinner = Y.Base.create(
‘Spinner’,
Y.Widget,
[Y.MakeNode],
{
// instance members …
},
{
// static members
}
);


You can add MakeNode along any number of suitable extensions for
Widget, such as WidgetParent, WidgetChild, WidgetStdMode, etc.
MakeNode adds two protected methods to be used by the developer,
_makeNode and _locateNodes,

and it will read from several static properties, if found.


All members of this extension are either protected or private
since they are meant to be used by the component developer and not by
the implementer using those components, who should not be bothered
with them. Remember to check the “Show Protected” option when viewing
the API
docs
.


Defining the template


The first thing you will normally do is to define the template for
your component. For the Spinner, our template will be:


_TEMPLATE: [
'<input type="text" title="{s input}" class="{c input}">',
'<button type="button" title="{s up}" class="{c up}"></button>',
'<button type="button" title="{s down}" class="{c down}"></button>'
].join(‘\n’),

The default template will usually be named _TEMPLATE
and declared along the other static properties of the class, such as
ATTRS. MakeNode will use this template
if none other is explicitly provided. The template is made of plain
HTML plus a series of placeholders enclosed in curly brackets, each
made of a single character (the processing code) and followed by one
or more arguments. The placeholders and what they produce are:



  • {@ attributeName} configuration
    attribute value


  • {p propertyName} instance
    property value

  • {m methodName arg1 arg2 ….}
    return value of the given method. The processing code is followed by
    the method name and any number of arguments separated by whitespace.


  • {c classNameKey} CSS className
    generated from the _CLASS_NAMES static
    property (see The _CLASS_NAMES property section below)


  • {s key} string from the strings

    attribute, using key as the
    sub-attribute.


  • {? condition
    valueIfTrue valueIfFalse}
    much like the ?: operator of
    JavaScript, evaluates to valueIfTrue if
    condition is truish, valueIfFalse

    otherwise.


  • {1 condition
    valueIfOne valueIfMore
    } used
    to produce singular/plural words based on the value of condition.


  • {} any other value will be
    handled just like Y.substitute does.


For example, {@ value} will translate
to this.get(‘value’) while {p
value}
translates to this['value'].


When placeholders have arguments, like {m},

{?} and {1},
strings must be enclosed in double quotation marks. Numbers, booleans
and null (all unquoted) will be parsed
to their proper data types. Placeholders can be nested. The {?}
and {1} placeholders will usually
contain a nested placeholder for the condition and quite possibly for
their values, for example:


{p qty} {1 {p qty} "unit" "units"} 

If the property qty is 1, it will
evaluate to "1 unit", for 2 or
more it will return "2 units"
and so on. A more elaborate version dealing with zero would be:


{? {p qty} "{p qty} {1 {p qty} "unit" "units"}" "none"} 

Note that the result of processing the inner placeholders, if a
string, must be enclosed in its own set of quotes.


To include a double quote inside a quoted string, use \\",
the double backslash being required because JavaScript will interpret
a single one and discards it before it gets to MakeNode. Only double
quotes are allowed; MakeNode does not use eval()
so the parser is limited but safe. Anything but numbers, null,
booleans and double quoted strings will be ignored.


The {?} placeholder is also handy to
use with checkboxes and radio buttons. It can be used to produce the
string "checked" depending on
the truth value of the processing instruction code that follows.
Thus, <input type="checkbox" {? {m
getLength} "checked" ""}/>
will produce
a marked checkbox if the getLength

method returns anything but zero.


For the {c} placeholder, we need to
have a _CLASS_NAMES property defined.


Further placeholders can be added to MakeNode by adding them into
the _templateHandlers hash.


The _CLASS_NAMES
property

Along with the ATTRS and _TEMPLATE
static properties, we can define a _CLASS_NAMES
static property which points to an array of strings. Each of those
strings will be used to generate a className. Thus _CLASS_NAMES:
['input']
will produce the className "yui3-spinner-input".
Those classNames are stored in an instance property this._classNames.
The {c input} placeholder in the
template above will be replaced by "yui3-spinner-input".
I call the strings listed in _CLASS_NAMES,
such as ‘input’, the “className keys”
since they can be used as a key to refer to the actual className or
the elements containing those classNames, as we’ll see later.

You can use the _CLASS_NAMES property
to generate any number of classNames, whether you use them in the
template or not. You can still reach those extra classNames from
within this._classNames. The className
is generated using the yui3 prefix
followed by the value of the NAME static
property turned lowercase, and then the string given in _CLASS_NAMES
(this last one will not be turned lowercase), all separated by
hyphens. The _classNames hash will also
contain the classNames for the boundingBox

and the contentBox, the first under the
"boundingBox" key and the second under
the "content" key. Widget
assigns to the boundingBox the
classNames derived from the values of the NAME
property of each of the classes in the inheritance chain, starting
with yui3-widget. MakeNode stores into

this._classNames only the top-most
className for the boundingBox.


If the WidgetStdMod module is loaded, MakeNode will also generate
entries for its HEADER, BODY
and FOOTER sections with those same
keys, which are also the constants defined in that same module.


If a component is several levels away from Widget, like

SuperSpecialSpinner inheriting from
SuperSpinner which inherits from Spinner
which inherits from Widget, and if any
or all of them have _CLASS_NAMES
properties defined, MakeNode will produce classNames for all of them
and store them in this._classNames. You
don’t need to include at each level the names already declared in the
previous levels. In fact, it is better that you don’t since the
classNames produced at each level will use the value of the NAME

property of that level. Thus, in SuperSpecialSpinner,
{c input} will still result in
"yui3-spinner-input" and not
"yui3-superspecialspinner-input"
and so it will keep your CSS file still valid.


The {s} placeholder


Widget has a strings configuration
attribute defined, though it is not initialized with any value. This
attribute is meant to hold strings that are visible to (or, via
screen readers, read to) the user. It is important that you never
include visible strings directly in the template. This is not a
requirement of MakeNode — it has never been a good idea at all. All
strings that are to be viewed by or read to the user should always be
placed in the strings attribute. The

strings attribute contains a hash where
each individual text is located by its key. The Spinner component has
the following strings, which you can see used in the template above.


strings: {
value: {
input: "Press the arrow up/down keys for minor increments, page up/down for major increments.",
up: "Increment",
down: "Decrement"
}
},


The best part of doing this is that your component can be localized
to other languages very easily by developers using your component.
When creating an instance of Spinner, you might do:


var mySpinner = new Spinner({strings: Y.Intl.get(‘spinner’)});

Setting the configuration attribute strings
in this way replaces the default strings
values with those from the language resource file using the language
previously defined. The {s} placeholder
accesses the strings stored in the strings
attribute, either the default ones or the translated ones, if set.
The {s xxxx} placeholder is almost like using {@ strings.xxxx}
except that the localized replacement strings can have placeholders which will be
further processed. This is important for translations since syntactical order varies
from language to language and this allows rephrasing the text, including its
placeholders to suit any language. Strings can also be accessed using
{@ strings.xxxx.yyyy.zzzz}, which will allow access to strings
nested deeper down and will prevent further substitutions. Curly braces can be included
in a text by using {LBRACE} and {RBRACE} as placeholders.

Using _makeNode in renderUI


We use the template to create the markup for our component. To do
so, we can call MakeNode’s _makeNode
method, like this:


renderUI : function() {
this.get(‘contentBox’).append(this._makeNode());
},


This will fill in the contentBox of our
widget with the markup from processing the template. The _makeNode
method returns an instance of Y.Node

which can be appended or inserted anywhere or just held for later
use. It does not return a string, it produces a Node
instance. (If you do need a string and not a Node, you can use
the _substitute method, which requires that you pass in a template.)


The _makeNode method takes two
optional arguments: a reference to a template and an object to fill
in placeholders, as Y.substitute does.
In our simple Spinner example there is a single template for the
whole widget but other widgets might require bits and pieces made out
of several templates. In that case, you would usually call _makeNode

with no arguments for the main part and call it once again with
different templates to fill in the extra parts. The example
contains this renderUI method:


renderUI: function () {
var fieldset = this._makeNode();
this.each(function (item) {
fieldset.appendChild(this._makeNode(MultipleTemplates.RADIO_TEMPLATE, item));
}, this);
this.get(‘contentBox’).append(fieldset);
}


The first call to _makeNode returns a
Node instance stored in the variable

fieldset. The sample component is also
extended with Y.ArrayList so the
RADIO_TEMPLATE will be filled with
values taken from the items stored in the array list and the
resulting Nodes appended to the fieldset
before it is finally appended to the contentBox.
The special placeholders such as {@} or
{p} will still refer to attributes or
properties in the main object. The nested items will be processed
just as Y.substitute would.

The _locateNodes method


MakeNode further provides a _locateNodes
method which will try to locate all the elements with the classNames
declared in _CLASS_NAMES. To locate
specific elements you can pass any number of className keys,
otherwise, _locateNodes tries them all.
For each element found of each className, _locateNodes
will produce a private instance property using the underscore prefix
followed by the key name and the "Node"
suffix. Thus, in our Spinner example, _locateNodes

will generate the properties _inputNode,
_upNode and _downNode.
If several elements have the same className, _locateNodes
will return a reference to the first of them. If an element is not
found, no variable will be created.


In the Spinner component we use _locateNodes
after creating the markup:

renderUI : function() {
this.get(CBX).append(this._makeNode());
this._locateNodes();
},


The _EVENTS static property


One further property can be defined along the lines of _TEMPLATE
and _CLASS_NAMES and that is _EVENTS.
_EVENTS will contain a hash made up of
class name keys, each containing a hash of event types and methods to
handle them. It is better explained with an example:

_EVENTS: {
input: ‘change’, // calls this._afterInputChange
boundingBox: [
{
type: 'key',
fn:'_onDirectionKey', // calls this._onDirectionKey
args:((!Y.UA.opera) ? "down:" : "press:") + "38, 40, 33, 34"
},
'mousedown' // calls this._afterBoundingBoxMousedown
],
document: ‘mouseup’, // calls this._afterDocumentMouseup,
Y: ‘broadcastingObject:someEvent’ // calls this["_afterYBroadcastingObject:someEvent"]
},


_EVENTS is an object (a hash) with any
number of entries. The names of the properties, that is, the keys of
the hash, identify the nodes whose events we will listen to. They are
the same className keys defined in _CLASS_NAMES.
There are several extra special keys:


  • "boundingBox" will
    refer to the bounding box itself.


  • "document" refers to
    the document containing this widget.


  • "THIS" refers to the
    widget itself


  • "Y" refers to the Y

    instance.



If the Widget has been extended with WidgetStdMod as well, the
keys HEADER, BODY
and FOOTER will refer to those sections
since they will be available in the _classNames
hash. JavaScript does not need the keys to be quoted if they are
valid identifiers so none of the above need to be quoted.

The _EVENTS property is processed
after the renderUI, bindUI
and syncUI methods have been called so
the widget is expected to be already inserted within the document
body, otherwise the "document" identifier will
fail.


For each of those elements there is an event identifier or an
array of event identifiers. An event can be identified by the type
of event to listen to or an object with further details. By default,
MakeNode will use as a listener a method named using the "_after"

prefix followed by the element identifier with its first character
capitalized followed by the event type with its first character
capitalized. The code block above shows the methods called for each
event.


An event identifier can also be an object with properties type,
fn and args.
The type is mandatory and indicates the
type of event being listened to. The fn
property gives the name of the method that will listen to the event
thus avoiding the automatic naming. Since _EVENTS

is a static property, it has no access to this
so it cannot take an actual reference to a method, only its name.
The args argument can be used to pass
further arguments to the caller such as with the key
event which requires a keys specification.


MakeNode will use Node.delegate to
listen to events on elements within the boundingBox,
while it will use Node.after to listen
to events from the boundingBox and the
document body. It will use this.after to
listen to events under the THIS key and

Y.after for listeners listed under the Y
key. All events are listened to using after event listeners
since they are meant to make the widget respond to events, not to
filter the behavior of the object that fires them so in no case these
events can be prevented or stopped. (Note: listening to the key
event on any nested element works only with version 3.4.0pr1 and
above, since delegation of the key event
was not available before. All the other features work with previous
versions as well).


The _EVENTS declarations are
cumulative when components inherit from one another. Each class in
the inheritance chain will have its own _EVENTS

declaration processed separately.


The _ATTRS_2_UI static property


Events go both ways, from the UI to the component and from the
component to the UI. The first are handled by the _EVENTS
property. Then there are the events fired by attribute value changes
that need to be reflected in the user interface. As I mentioned in
the previous article, when there are any secondary effects from
changing a configuration attribute, they should be handled by change
event listeners, not by the optional setter
method of the attribute, which should only deal with normalizing the
value being set. The UI should reflect the state of the configuration
attributes, first in syncUI, when being
initialized and then on every attribute change event. For the latter,
we need to attach an event listener, which we would normally do in
bindUI. Widget already provides a
mechanism to make that simple, which I described in the comments to
the previous article.

Widget uses the instance property _UI_ATTRS
that contains an object with two further properties, SYNC
and BIND. Each of these is an array
listing the names of the configuration attributes to be initially
synched and then listened to in order to keep the UI reflecting
current values. Widget expects each of those entries to have a method
associated with it, named after the attribute name prefixed by _uiSet
with the first character of the attribute name converted to uppercase
to have the method name in proper camel case. Thus, if "value"
was listed in any of the _UI_ATTRS

arrays (in either SYNC or BIND),
Widget would expect to find a _uiSetValue
method. This method will receive two arguments, the value
being set and the src of the change.
This is the code for our Spinner _uiSetValue

method:


_uiSetValue : function(value, src) {
if (src === UI) {
return;
}
this._inputNode.set(VALUE, this.get(FORMATTER)(value));
},


All the uppercase identifiers you see in this piece of code
correspond to string constants declared elsewhere, to allow the YUI
compressor to do its job better. The method, basically, sets the
value HTML attribute in the <input>
box to the new value set, after being formatted. The reference to the
textbox was provided by _locateNodes.
The src argument is initially checked to
see if set to the string value ‘ui’. If
this is so, no action will be taken. This is to avoid endless loops.
If the user enters something in the input box, its value would go
into the value configuration attribute
which then would fire a valueChange

event, which would get _uiSetValue
called which, if unchecked, would then go and change the value of the
input box, which would trigger the whole process again. Thus, in
_uiSetValue, if we know the change comes
from the UI, we do nothing and so break the loop. However, this
requires another piece of code elsewhere. In the listener for the DOM
event, when we set the configuration attribute, we use the third
optional argument to set, like this:


_afterValueChange : function(ev) {
this.set(VALUE, ev.newVal, {src: UI});
}


It is up to us to ensure that changes coming from the UI are flagged
thus and then check that same flag to avoid loops. Do use the
identifier src when setting the value of
the attribute, not source which will not
be recognized.

With all this said, I haven’t yet talked about the static property
_ATTRS_2_UI mentioned in the heading of
this section. As the comments in my previous article shows (through
the blunders I made in them), making sure that all attributes
affecting the UI are properly listed is somewhat messy. You should
never initialize _UI_ATTRS from scratch
since Widget already lists a whole lot of attributes and those would
be lost. You have to concatenate new attribute names over the
existing ones, which is somewhat hard to remember how to do it right.
To make it simple, MakeNode will read from the static property
_ATTRS_2_UI and do that concatenation
for you. It will concatenate all such lists from each and every class
in the inheritance chain so at each level each class can handle its
own attributes. In Spinner, we have:


_ATTRS_2_UI: {
BIND: VALUE,
SYNC: VALUE
},


MakeNode will accept both an array of names or a single attribute
name, as in this case.


The question naturally arises, why two lists, one for binding the
other for syncing? SYNC is used the
first time around, after the renderUI

and bindUI methods, if they exist, are
called and before syncUI while those
listed in BIND will be bound to the
corresponding attributes for later changes. Quite often the SYNC
array has fewer entries than the BIND
list and this is because the template for the component might already
have the very same default value as the configuration attribute and
there is no need to do an initial syncing. So, if the default value
for the value configuration attribute is
an empty string and the <input>

element in the template has no value
attribute, then there is no need to sync them on initialization.


Attributes listed in BIND will have
their _uiSetXxxx
methods called in any order, as attributes can be set in any order.
Attributes listed in SYNC will be called
once in the order in which they are listed with those of ancestors before their inheritors, so if one is dependent
on another (which they shouldn’t), the order might be important.

MakeNode will check for duplicate entries in any of these arrays.
If any appear, it means that a class our component inherits from
already handles this attribute and any new declaration would most
likely overstep the _uiSetXxxx
handler for it. Incidentally, MakeNode also checks for duplicate
entries in _CLASS_NAMES, which can also
cause conflict in some, though not all, circumstances. MakeNode will
write a message to the log for any such error.


The _PUBLISH property


Finally, the _PUBLISH static property
will list those events that have to be published. It contains a hash
using the name of the event as its keys and an object literal of
configuration attributes as its values. It will publish all the
events listed in any such property in all the inheritance chain. The
same event name can be published in a class and in any class
inheriting from it, which will make the configuration attributes of
later ones override the ones in the older ones. For example, you
might want to make an existing event broadcast globally. Just as with
the _EVENTS property, since _PUBLISH

is a static property without access to this,
when specifying functions, it is the name of the method, as a string,
that needs to be given.


Conclusion


MakeNode provides a very simple template processor, with
functionality that is highly integrated with the Widget foundation
class. It also provides helper methods to create classNames to be
used in the template and to use those names to locate and refer to
the nodes created. It also provides the means to hook into the events
generated both by the UI and the component itself and associate each
with a method. It does all these things, while taking care to respect
the inheritance chain straight up to Widget and any level of classes
you may define.


It does not provide for absolutely all possibilities, but covers a
good range of them. Nevertheless, it does not preclude you from
adding extra functionality. You might rarely have to write a bindUI
or syncUI method if you use the glue
provided by MakeNode, but you may do so, since MakeNode does not use
them.


As a bonus to those who had the patience to read this far, I have
also modified Anthony Pipkin’s Button set of gallery components and
made an Accordion and TimeSpinner components, all available in the

Gallery.

SatyamAbout the author: Daniel Barreiro (screen name Satyam) has been around for quite some time. The ENIAC was turned off the day before he was born, so he missed that but he hasn’t missed much since. He’s had a chance to punch cards, program 6502 chips (remember the Apple II?), own a TRS-80 and see some fantastic pieces of operating equipment in his native Argentina which might have been in museums elsewhere. When globalization opened the doors to the world, his then barely usable English (plus an Electrical Engineering degree) put him on the career path which ended in a 5-year job in the Bay Area back in the days of NCSA Mosaic. Totally intrigued by the funny squiggles a friend of his wrote in his plain text editor, full of <’s and >’s, he ended up learning quite a lot about the world of frontend engineering. It’s been a long journey since COBOL and Fortran. Now he lives quite happily semi-retired in the Mediterranean coast close to Barcelona, Spain.

By SatyamSeptember 12th, 2011

In the YUI 3 Gallery: Geo

Geolocation is one of the more exciting HTML5-related technologies to appear in browsers, and the Geo Gallery module gives you access to location information. The W3C Geolocation API provides a simple interface to access the user’s location from JavaScript. The following code accesses the user’s current location in a supporting browser:

navigator.geolocation.getCurrentPosition(function(result) {
    //success handler
}, function (result){
    //failure handler
})

When this code is executed, the browser pops up a message asking for the user’s permission to reveal their current location. The dialog displayed in Firefox looks like this:

Geolocation dialog in Firefox

If the user denies permission, or an error occurs while trying to get the current position, the failure handler is called. Otherwise, the success handler is called with information about the current location. That information comes in the form of latitude and longitude coordinates (other information may be available as well, depending on the implementation).

The W3C Geolocation API is supported in Internet Explorer 9+, Firefox 3.5+, Safari 5+, Chrome, and Opera 10.6 as well as on Mobile Safari and Webkit on Android, making it fairly ubiquitous.

The Geo module uses the Geolocation API when it’s available, and falls back to an IP-based lookup via the YQL pidgets.geoip open table when not available or if there is an error. This table is exceptionally useful because you can lookup location information for a specific IP address, or you can omit the IP address and it will return the location information for the IP address making the request. The latter part ensures that you need to make only one request to get location information instead of two (other solutions use one to get the IP address and then one to get the location information for that IP address).

In typical YUI fashion, the Geo module provides a streamlined interface for accessing geolocation information. Instead of providing two callback functions, one for success and one for failure, just pass in one. The result object has a success property indicating if the call succeeded:

YUI({
    gallery: 'gallery-2011.04.27-17-14'
}).use('gallery-geo', function(Y) {
 
    Y.Geo.getCurrentLocation(function(response){
 
        //check to see if it was successful
        if (response.success){
            console.log(response.coords.latitude);
            console.log(response.coords.longitude);
        }
 
    });
 
});

When a geolocation call completes successfully, the success property is true and response.coords is filled with at least two properties: latitude and longitude (if the native API is used, then all available properties are copied to this object). There is also a source property on the response object that is either “native”, if the information was retrieved from the native API, or “pidgets.geoip”, if it was retrieved by YQL. If an error occurs, or if the user declines to provide location information, then success is false.

If the Geolocation API has an error, the Geo module will try IP-based lookup instead. If, however, the user declines to provide information, the IP-based lookup is not performed.

Keep in mind that the native API is much more accurate than IP location, so you won’t get the same quality results in browsers without native geolocation support. However, the Geo module is a good first step to providing location-based experiences to your users.

By Nicholas C. ZakasMay 6th, 2011

Quick Edit mode for YUI 3 DataTable

Even though YUI 3 DataTable does not yet have inline editing of individual cells, it is relatively simple to implement Quick Edit mode. The QuickEdit plugin for DataTable in the YUI 3 Gallery allows all the visible values in a DataTable to be edited simultaneously.

(Click the screenshot to play with this example.)

Overview

As with the YUI 2 version, the core idea of Quick Edit mode is to swap out all the cell formatters with new ones which populate the cells with form elements, e.g., input fields or dropdowns. This is done when start() is called, based on the configuration described below. After the user is finished, you can call getChanges() to get the changed values and then persist them. To exit Quick Edit mode, call cancel(). (It is named cancel instead of stop to remind you that it discards all changes.)

Since the Quick Edit gallery module is a plugin for DataTable, you need to plug it in to your datatable before you can use it:

my_table.plug(Y.Plugin.DataTableQuickEdit);

This stores the plugin in the qe member of the datatable, so you must call the plugin’s functions like this:

my_table.qe.start();

Configuration

Quick Edit adds two new configuration attributes to all columns: quickEdit and qeFormatter.

If a column’s quickEdit property is defined, the column will be editable in Quick Edit mode. To accept all the defaults, you can simply set quickEdit:true. For more control, you can pass an object with the following properties:

formatter

The cell formatter which will render an appropriate form field: <input type=”text”>, <textarea>, or <select>. By default, the cell formatter Y.Plugin.DataTableQuickEdit.textFormatter is used for all cells to produce input elements. To get a textarea element, configure a column to use Y.Plugin.DataTableQuickEdit.textareaFormatter instead.

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.displayMessage(...) 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 Y.FormManager.Strings (provided by gallery-formmgr-css-validation) 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 Y.Plugin.DataTableQuickEdit.readonlyLinkFormatter. For email addresses, use Y.Plugin.DataTableQuickEdit.readonlyEmailFormatter. You can also write you own custom, read-only formatter. Simply follow the normal rules for constructing a DataTable cell formatter.

Missing Features

Due to a bug in YUI 3.3.0 DataTable, the td element passed to a column formatter is actually from the previous column. This made it too troublesome to support copy down, where a button in the first row lets you copy the value down to all other rows.

The bug also required a complete reworking of the basic Quick Edit cell formatters to return text instead of manipulating the DOM. This is why custom cell formatters are not officially supported in this initial release. If you are adventurous, you can still build them, but keep in mind that you will need to rewrite them, including adding in support for copy down, once the bug in DataTable is fixed.

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.

By John LindalApril 19th, 2011

Filtering the Data Displayed by YUI 3 DataTable

In addition to sorting, which is supported by YUI 3 DataTable, it is often useful to be able to filter the data and display a subset of the available rows. The Query Builder widget in the YUI 3 Gallery provides a UI for constructing a simple filter expression.

(Click the screenshot to play with this example.)

History

The first version was written by a colleague working with me on the Yahoo! Publisher Network (YPN). (He left soon afterwards to attempt honest employment. Following the precedent set by Jamie Zawinski, he opened a pub to sell beer — home brewed, no less! But I digress….) After hacking together the first version of Query Builder, he made the mistake of showing it to me. A few days later, he complained, “You rewrote the whole thing!” In fact, I have rewritten it several times over the years. YPN is gone, but the latest YUI 2 version of Query Builder powers all data tables in APT, Yahoo’s display advertising management platform. The port to YUI 3 is actually the least amount of work I have had to do to generate a new version!

How the example works

The core of the example is (1) the Query Builder configuration which specifies how the user can filter the data and (2) the extension to Y.DataSource.Local which implements the filter. (For server side pagination, you would send the filter data to the server and bake it into your SQL query.)

To configure Query Builder, the example first defines a list of the variables that can be filtered:

var var_list =
[
	{
		name: 'title',
		type: 'string',
		text: 'Title'
	},
	{
		name: 'year',
		type: 'number',
		text: 'Year',
		validation: 'yiv-integer:[0,2100]'
	},
	{
		name: 'quantity',
		type: 'number',
		text: 'Quantity',
		validation: 'yiv-integer:[0,]'
	}
];

Each variable is assigned a name (matching the key in the DataTable column configuration) and a type. The default types are ‘string’, ‘number’, and ‘select’, but you can extend this by building custom plugins (see below). For each type that you use, you must also define a set of operators:

var ops =
{
	string:
	[
		{ value: 'contains',    text: 'Contains' },
		{ value: 'starts-with', text: 'Starts with' },
		{ value: 'ends-with',   text: 'Ends with' }
	],

	number:
	[
		{ value: 'equal',   text: '=' },
		{ value: 'less',    text: '<=' },
		{ value: 'greater', text: '>=' }
	]
};

This specifies the operators that the user can apply to each variable type. (If you need different sets of operators for variables of the same fundamental type, you can clone the type. See the Plugins section below.)

Y.FormManager is used to validate the values entered by the user before the filter is applied. The validation key for each variable in the above Query Builder configuration provides CSS data which is interpreted by Y.FormManager.

If all validations pass, a request is sent to the data source. The extension to Y.DataSource.Local is quite simple. It merely filters the data before returning the results:

Y.extend(CustomDataSource, Y.DataSource.Local,
{
	_defDataFn: function(e)
	{
		var data = filterData(e.data, e.request.filter);
		var response =
		{
			results: data.slice(e.request.recordOffset,
						e.request.recordOffset + e.request.rowsPerPage),
			meta:
			{
				totalRecords: data.length
			}
		};

		this.fire("response", Y.mix({response: response}, e));
	}
});

The filter element of the request is obtained from QueryBuilder.toDatabaseQuery(), which returns a list of [variable, operator, value] tuples. Also note that the response must include information on the total number of records, since this changes based on the filter being applied.

filterData() simply loops over the tuples from toDatabaseQuery(), applying the filter operators defined in a two level lookup table:

var filters =
{
	string:
	{
		contains: function(value, filter)
		{
			return (value.indexOf(filter) >= 0);
		},
		'starts-with': function(value, filter)
		{
			return (value.substr(0, filter.length) == filter);
		},
		'ends-with': function(value, filter)
		{
			return (value.substr(-filter.length) == filter);
		}
	},

	number:
	{
		equal: function(value, filter)
		{
			return (parseInt(value, 10) == parseInt(filter, 10));
		},
		less: function(value, filter)
		{
			return (parseInt(value, 10) <= parseInt(filter, 10));
		},
		greater: function(value, filter)
		{
			return (parseInt(value, 10) >= parseInt(filter, 10));
		}
	}
};

After all this, DataTable simply displays what it receives from the data source.

Plugins

Y.QueryBuilder.plugin_mapping defines the mapping of type names to actual classes. You can augment this mapping in two ways:

  1. Clone an existing type by defining a new name for the same class. This allows different sets of operators for different variables of the same fundamental type.
  2. Create a new type by implementing the plugin API. Studying the source code for the existing plugins is the best way to get a feel for how this API works.

Generalizing Query Builder

Query Builder does not support parentheses, so you can either AND all the conditions or OR all the conditions. While it is possible to introducing parentheses into a graphical representation of a Boolean expression, all the designs that I have seen are too cumbersome to use. A textual representation is much simpler and easier to manipulate. Expression Builder incorporates Query Builder into a widget that allows constructing a textual representation without having to remember the syntax or type everything in by hand.

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.

By John LindalMarch 1st, 2011

Treeble with YUI 3 DataTable

The beta release of DataTable in YUI 3.3.0 gives us a very powerful component to play with. To kick the tires in a useful way, I decided to update my Treeble examples to use DataTable. (Treeble enables displaying hierarchical data in a table.)

To my delight, it was a breeze! All the hard work is done in TreebleDataSource, which extends YUI 3 DataSource, so all I had to do was plug it into DataTable by using Y.Plugin.DataTableDataSource and then configure the columns:

var ds = new Y.TreebleDataSource(...),
	pg = new Y.Paginator(...),
	table;

function sendRequest() {
	table.datasource.load({
		request: {
			startIndex:  pg.getStartIndex(),
			resultCount: pg.getRowsPerPage()
		}
	});
}

var cols = [
    { key: 'yui33-hack', label: '' },
    {
        key: 'treeblenub', label: '',
        formatter: Y.Treeble.buildTwistdownFormatter(sendRequest)
    },
    {
        key: 'title', label: 'Title',
        formatter: Y.Treeble.treeValueFormatter
    },
    ...
];

table = new Y.DataTable.Base({columnset: cols});
table.plug(Y.Plugin.DataTableDataSource, {datasource: ds});

To see the complete source code, refer to the live example.

The only flies in the ointment are:

  • The yui33-hack column. Due to a bug in YUI 3.3.0 DataTable, the td element passed to a column formatter is actually from the previous column. Thus, the first column in the table displays the twistdown, and the second column is empty.
  • Undefined values in the data are displayed as {value} instead of blanks (bug 2529858).

In order to make Treeble easier to use, I have added a Sam skin which styles to the CSS classes written out by the Y.Treeble formatters.

Enjoy!

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.

By John LindalJanuary 24th, 2011

In the YUI 3 Gallery: The Carousel Module

About the authors: Gopal Venkatesan (@g13n) works for Yahoo! in Bangalore where he is one of the deans of the frontend engineering community; Gopal has been the lead engineer on the YUI 2 Carousel project since the 2.6.0 release. He is also the author of the new YUI 3 Gallery Carousel module. Fabian Frank hails from Germany and works for Yahoo! in Beijing; Fabian has been with Yahoo since he finished his Master’s of Research at the University of Glasgow.

What is a Carousel?

A Carousel control provides a widget for browsing among a set of like objects arrayed vertically or horizontally in an overloaded page region. The “carousel” metaphor derives from an analogy to slide carousels in the days of film photography; the Carousel control can maintain fidelity to this metaphor by allowing a continuous, circular navigation through all of the content blocks.

The Carousel is part of a family of widgets that allow you to “overload” a space on the page, providing more content for that space than fits within its dimensions while providing an easy, intuitive mechanism for the user to discover and navigate the additional content. Accordions, Tabs, Trees and ScrollViews are other examples of this genre.

Why yet another Carousel control?

YUI 3 needs a robust, feature-rich Carousel (as we have in YUI 2). The design goal for this Carousel was to make it lean and clean and add additional configurations through plugins, taking advantage of YUI 3′s intrinsic support for modularity to preserve lightness and speed.

YUI 3 Widget Framework

One of the biggest advantages of writing custom widgets using YUI 3 is the Widget framework (more on the Widget framework: user’s guide, in-depth video introduction). Comparing the Carousel in YUI 2.8.2 to my YUI 3 Gallery Carousel, the YUI 3 version is lean and elegant. This is because most of the heavy lifting with respect to providing the foundation providing common set of widget attributes, a disciplined lifecycle, progressive enhancement support, etc. come with the Widget class.

The YUI 3 Widget framework also provides a consistent MVC pattern which promotes every widget to adopt separation of state methods versus UI update methods. This makes the code very clean and maintainable. In fact this is one of the important factors why the YUI 3 Carousel is better than its earlier YUI 2-based cousin.

The YUI 3 Plugin model allows developers to add new functionality or modify existing behavior to objects. This allows adding additional functionality on top of Carousel to dynamically pull elements through Ajax, etc. So, the YUI 3 Carousel does not have animation code baked into it, but instead I have created a plugin which adds animation support for cases where it is needed. This helps to keep the component very lightweight.

A Gallery Carousel for your own website

After reading about what a Carousel is and how it can help you to improve your website, you hopefully feel eager to get your hands dirty. Don’t worry, with our YUI 3 Gallery Carousel extension, implementing a carousel on your website is as easy as providing a bulleted list in HTML. That’s not just a sales pitch — that’s how we recently integrated a Gallery Carousel into the Yahoo! Sports Search Results Page.

A simple example

Let’s start with a simple example that will cover almost everything you need to know. The easiest way for you to use the new Gallery Carousel is to let YUI 3 automagically load it from Yahoo’s content delivery network. Recalling what a Carousel is, a scrollable list of items, we create a list in HTML. We include the list in a div, which allows our JavaScript to easily find and work with it. If you already have some data that is represented in a list-like way in your markup, you might also just put the carousel div around it and test your luck! It is very important to say that, although we are using an image example here, you can use Gallery Carousel for anything you want!

<div class="carousel" id="container">
  <ol>
    <li><img src="img/c1.jpg"></li>
    <li><img src="img/c2.jpg"></li>
    <li><img src="img/c3.jpg"></li>
    <li><img src="img/c4.jpg"></li>
    <li><img src="img/c5.jpg"></li>
    </ol>
</div>

Now that we have our data to work with, we want to enhance the looks by showing all five items using the Carousel widget. Assuming that you are already using YUI 3 this is a straightforward task. The only thing that you might not have seen before, depending on how deep you have been digging into YUI 3 and the Gallery in the past, is that we are specifying a Gallery version explicitly. This is necessary because we are using a brand new widget, which is not including in the Gallery build that YUI’s loader tries to load from by default. However, as YUI and YUI Gallery mature, this will not be necessary anymore in the future when the default build number is being increased.

YUI({gallery: 'gallery-2010.10.20-19-33'})
 .use("gallery-carousel", "gallery-carousel-anim", "substitute", function(Y) {
  var carousel = new Y.Carousel({ boundingBox: "#container",
   contentBox: "#container > ol" });
  carousel.plug(Y.CarouselAnimPlugin,{animation:{speed: 0.7}});
  carousel.render();
});

(By the way, if you are interested in getting brand new stuff you can visit the YUI 3 Gallery repository on github or Gopal’s fork, where he develops Carousel. If you find a bug, we are always happy to hear about it.)

Back to our example… YUI will take it from here. The loader automatically pulls Gallery Carousel and its dependencies from Yahoo’s content delivery network. After that, the Carousel is being initialized and displayed. The user can then click the left or right arrow to scroll around. Please forgive me for bringing in one line of unnecessary complexity, but I couldn’t resist. I used Y.CarouselAnimPlugin to let our carousel slide smoothly from one page to the other instead of just displaying the next page instantly. Feel free to play with the speed parameter if you wish.

As you can see from the screenshot above, the Carousel is up and running. However, the CSS might not fit very well into the rest of your page. This leads us to our next section, where we’ll discuss how to customize Gallery Carousel.

Customizing Gallery Carousel

Now that you have your Carousel up and running and identified a use case for your website, you hopefully want to integrate it seamlessly. As mentioned previously, we did so for our Sports Search Results Page. If you want to increase the number of visible items, for example from three to four, you can do so by modifying the line which instantiates Carousel.

var carousel = new Y.Carousel({ boundingBox: "#container",
 contentBox: "#container > ol", numVisible: 4 });

Still not good enough? Yeah, right. Luckily CSS allows us to add our own style definitions and even overwrite the initial ones without touching any existing CSS. So your first step will probably be to remove the borders, because they are quite obtrusive. Just add the following CSS to your page header.

.YUI 3-carousel {
  border: none !important;
}
.YUI 3-carousel-nav {
  background: none !important;
}
.carousel ol li {
  border: none !important;
}

Now, that looks better. I’ve also used a negative margin to reduce the gap between my Carousel and the heading. However, we are still not completely there. I assume that you also want to use your own custom buttons, which integrate nicely into your page layout. For this example we will use the same buttons that are also used on Yahoo’s search result pages. This requires a bit more, but still simple, CSS.

.YUI 3-carousel-button {
  background: url("sprite_button.png") no-repeat scroll 0 0 transparent !important;
  height: 20px !important; width: 28px !important;
}
.YUI 3-carousel-nav-item {
  background: url("sprite_button.png") no-repeat scroll 0 0 transparent !important;
  background-position: -133px 0 !important;
}
.YUI 3-carousel-first-button {
  background-position: -90px 0 !important;
  margin-right: 35px !important;
}
.YUI 3-carousel-first-button-disabled {
  background-position: -60px 0 !important;
  margin-right: 35px !important;
}
.YUI 3-carousel-next-button {
  background-position: -30px 0 !important;
}
.YUI 3-carousel-button-disabled {
  background-position: 0 0 !important;
}
.YUI 3-carousel-nav-item-selected {
  background-position: -121px 0 !important;
}

We will leave it to that for today and hope you feel ready to get started. At least that was all that we needed. However, depending on how big your site is and how interested you are in its performance, there are general thoughts about loading something from a third party content delivery network that also apply here. For example, Sidnei da Silva laid out some interesting thoughts in a blog post earlier this month. We would be happy to provide a How To that explains how a YUI widget and its dependencies can be moved to your own website, or even content delivery network, so you are able to keep the number of HTTP requests as low as possible. Let us know if you are interested, we are looking forward to your feedback!

More to Explore in the Gallery

The excellent team of Eduardo Lundgren and Nate Cavanaugh of Liferay have a Carousel component in the Gallery as well — certainly worth checking out if you’re in the market for this kind of control.

By Gopal Venkatesan and Fabian FrankDecember 13th, 2010
« Older Entries

Pages

  • About
  • Contribute
  • YUI Jobs

Recent Posts

  • YUI Weekly for May 17th, 2013
  • Yahoo’s International Team Is Hiring!
  • YUICompressor 2.4.8 Released
  • YUI 3.10.1 Released to Fix SWF Vulnerability
  • YUI Weekly for May 10th, 2013

Archives

Categories

  • Accessibility (25)
  • CSS 101 (6)
  • Design (51)
  • Development (590)
  • Frontend Jobs at Yahoo (13)
  • Graded Browser Support (8)
  • In the Wild (63)
  • Miscellany (11)
  • Open Hours (44)
  • Performance (23)
  • Releases (25)
  • Target Environments (11)
  • Yeti (3)
  • YUI 3 Gallery (29)
  • YUI Events (45)
  • YUI Implementations (55)
  • YUI Theater (146)
  • YUI Weekly (37)

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
© 2013 YUI Blog