November 21, 2009

working with dijit.form.Form

i've found myself using this kind of pattern lately and found that i've also been explaining it quite often as well. so to avoid any further explanations here's some of what i've learned about how to try and design your code to leverage what dijit.form.Form (and other widgets) can provide. NOTE: my examples are based on dojo 1.3.2 and there are small variations to be made if you use dojo 1.4 (which is almost ready for release). the principles remain the same.

i'm going to cover:

  • how to set and get values for forms and form widgets

  • creating widgets to help with using complex data

  • widget templating including dojoAttachPoint and dojoAttachEvent

  • and more...



dijit setters and getters


dojo's widget library (dijit) provides a uniform way to set and get properties of widgets. this is provided by the dijit._Widget base class and it used like so:

var widget = new SomeWidget();

// get the value property of the widget
widget.attr('value');

// set the value property of the widget
widget.attr('value', 'a new value');

the .attr function makes it convenient to be able to consistently set or get properties. there's basically 3 ways that it works. i'll explain 2 of them - the 3rd way is also very powerful and makes use of the attributeMap to map widget properties to node properties.

first, .attr looks to the widget for some specially named functions to help with setting/getting properties. the naming convention is that if you're working with property foo then you can define a function called _setFooAttr which will be called in response to widget.attr('foo', value); and you can define a function called _getFooAttr which will be called in response to widget.attr('foo');

the _setFooAttr function will be passed the 2nd parameter of widget.attr('foo', value); and _getFooAttr should return a value to represent the foo property.

the 2nd option that .attr will use, is it will directly update/read the property eg. widget.attr('foo', value) is the same as widget.foo = value; and var value = widget.attr('foo'); is the same as var value = widget.foo;

just remember that if you provide the custom function to set/get then you will need to update the object property directly if you need it updated. widget.attr('foo', value); won't update widget.foo if you have _setFooAttr defined as a function in your widget. also, if you provide either of the custom functions, you are not required to provide the other one. so, for example, you might provide the setter function but not the getter function. in this case, the setter function will be used to set and the object property will be used for get.

ok, so knowing this, we can keep this in mind when we're creating widgets and leverage it as needed.

how dijit.form.Form uses setters and getters


now that we know about .attr we can look into how it's used for setting/getting values for dijit.form.Form. the value for a form is generated from the elements contained in that form. for dijit.form.Form it looks for all the widgets that are the descendants of it's containerNode and if that widget has a name property then the form value will have a property name that matches the value of the name of the widget. so, for example, if the widget's name is foo then the form's value will contain a foo property. the value of this property is obtained by getting the widget's value via widget.attr('value');

setting a dijit.form.Form value is simply the reverse of this. the form looks to the value being set and for the properties of that value, it looks for a child widget with a name to match that property and then tries to set that widget's value via widget.attr('value', formValue.widgetName); - this explanation does not follow the code exactly but the end result is the same. you should read the code if you want exact details :)

so, now that we know this, it's easy to see how if we have a dijit.form.TextBox with the name property set to firstName inside a form then we would expect the following

var form = dijit.byId('aForm'); // get a handle to the form

var value = form.attr('value'); // value.firstName will be the value of the dijit.form.TextBox

form.attr('value', {firstName: 'Bob'}); // the dijit.form.TextBox will now have the value 'Bob'


well this is easy for forms where we have a fixed number of fields and our data is 'flat' - ie, each widget just returns a string, a number, a boolean or even a Date.

what do we do though if we want to do something a little more complex? for example, we might have a form where a user can enter their name (first, last) and their address (street, city, state, zip, country). we could have a form where we have fields with names to match each of these but what if we wanted to treat the address as an object so that at the top-level, our form only has 3 properties (first, last, address)? well, we would need to have 3 widgets that have these names - first, last, address. ok, that's easy but dijit doesn't have an address widget! so... now it's time for us to write our own widget.

based on what we know about .attr and dijit.form.Form here's what our widget will have to do:

  • widget.attr('value'); will have to return an object that has properties called street, city, state, zip, country

  • widget.attr('value', formValue.address); will need to take an object with the above properties and display them appropriately

  • and our widget needs to have a name.



so, it looks like we will need to know how to build our own widgets to do this. fortunately, if you know enough about dojo to have been able to build a form with fields for street, city, state, zip and country then you're just a step or 2 away from being able to break out those fields into a widget.

well, every good tutorial has sample code, right? and i'm going to follow that trend but my code relates to a slightly more complex problem. once we work through the sample, then you should be able to come back and write the code for the problem described above.

the sample code will build a form that will allow a user to enter a schedule. so, they can choose the date for their schedule and then add tasks/events for that date. each of these tasks, will have a time and a description. the extra level of complexity that we have from the first problem (described above) is that we are going to allow the user to add or remove tasks which will mean we have a dynamic number of fields in our form. keep in mind, this problem was contrived just to demonstrate the principles in the code - it will be lacking in many ways but it serves the purpose of demonstrating the principles i'm trying to explain.

our high level design


basically, our design is driven by our data format. at the top-level, our data has 2 properties:

  • date - the date of our schedule

  • tasks - the list of tasks we have scheduled for this day



our list of tasks will be an array of objects. each of these objects will have the following properties:

  • time - the time the task begins

  • description - a description of the task



from this, we see that we need a widget for selecting a date (we'll choose dijit._Calendar) and we need a widget that can manage the list of tasks. since we can add and remove tasks from this list, then it seems like it would also be convenient to build a widget to represent a single task. this task widget will then need a widget for selecting a time (dijit.form.TimeTextBox) and another widget to enter a description (dijit.form.TextBox).

ok, so now that we have a high level design, we can start getting into the details of building these widgets. let's start with the widget for a single task and then work our way back up to the top-level of the form. you can look at the finished example if you want to see what we're working towards.

my.widgets.Task


this task widget will then need a widget for selecting a time (dijit.form.TimeTextBox) and another widget to enter a description (dijit.form.TextBox).


to keep this simple, we're going to leverage dijit's templating class - dijit._Templated. this allows us to define the high level DOM structure of our widget using html. based on our design, we can see that this should be fairly simple. the template for our Task widget is simply:

<div class='task'>
<div dojoType='dijit.form.TimeTextBox' dojoAttachPoint='timeInput'></div>
<div dojoType='dijit.form.TextBox' dojoAttachPoint='descInput'></div>
</div>


this is really simple. we have the outer div which becomes the domNode of our widget. we have put a class on this node so that we can easily style any part of this widget by including that class in our selector if necessary. this outer node has 2 children - 1 for our time widget and 1 for our description. using dojoType we can specify which widgets we want to have at the respective places in the DOM. in our javascript file that defines our widget, we just need to remember to set the widgetsInTemplate property to true so that our template gets parsed and those nodes get turned into widgets. we have also added dojoAttachPoints to our template. this allows us to map those widgets (in our template) to properties of our Task widget. by doing this, we can easily reference those individual widgets through those properties and one thing this will do is allow us to easily set or get the values of those widgets. so far, so good!

next we'll look at the javascript file which goes along with this template:

dojo.provide('my.widgets.Task');

dojo.require('dijit._Widget');
dojo.require('dijit._Templated');
dojo.require('dijit.form.TimeTextBox');
dojo.require('dijit.form.TextBox');

dojo.declare('my.widgets.Task', [dijit._Widget, dijit._Templated], {
templatePath: dojo.moduleUrl('my.widgets.templates', 'Task.html'),
widgetsInTemplate: true,

_setValueAttr: function(value){
if(value){
this.timeInput.attr('value', value.time || null);
this.descInput.attr('value', value.description || '');
} else {
this.timeInput.attr('value', null);
this.descInput.attr('value', '');
}
},

_getValueAttr: function(){
return {
time: this.timeInput.attr('value'),
description: this.descInput.attr('value')
};
}
});

ok - that's a little more code than the last block so let's break it down.

the first line is used by dojo's packaging system. this line identifies that this file provides the code for the 'my.widget.Task' package. the next few lines also relate to dojo's packaging system. they are just specifying the dependencies of this 'class'. the real meat of what we're looking at is inside dojo.declare. you can see that we are extending dijit._Widget and dijit._Templated. if you need more details about what's happening here then i'll leave it up to you to find out about this. the simple explanation here is that these are the 2 classes you most typically use to build a widget based on a template.

the next 2 lines define the path to our template and also let the templating system know that we have widgets in our template so it needs to parse the template to generate those widgets.

that leaves the 2 functions. if you can remember all the way back to the start, you'll know that these 2 functions are the custom functions used as setter/getter.

the setter checks if a parameter is passed in and sets the values of the time (this.timeInput) and description (this.descInput) widgets based on the value passed in. you can see how we're making use of the dojoAttachPoints that we indicated in our template and we're also using .attr to set the value of those widgets. this is the beauty of a standardized interface.

the getter simply forms an object with property names we're expecting (time, description) and assigns values to the properties based on the .attr getter for each of the widgets.

now that we have our Task widget, we can use it to build a list of tasks.

my.widgets.TaskList


ok, we're building on what we've learned already - no point in stopping here, we're going all the way :) i'll try to keep you a little entertained amidst all this learning.

...we need a widget that can manage the list of tasks.


this widget will manage our list of tasks - it will provide ways to:

  • add a task

  • remove a task

  • set the value from a list of tasks

  • return the list of tasks represented by this widget



add a task


we'll include a single button that will add another task with the current time and a default description of 'New Task'

remove a task


for each task that we add, we'll add a button which will provide a way for the user to remove that task

set the value from a list of tasks & return the list of tasks represented by this widget


for this, we'll write a custom setter and a custom getter - _setValueAttr and _getValueAttr

dijit._Container


since this widget will be a container for other widgets, we will mixin dijit._Container which will provide us with some functions to help managing adding/removing other widgets under our control. the only thing we really need to consider for this is that we need to designate a node as our containerNode so that these helpful functions will work :)

with all of this in mind, here's the template for this widget:

<div class='taskList'>
<div dojoAttachPoint='containerNode'></div>
<div class='add'>
<div dojoType='dijit.form.Button' dojoAttachEvent='onClick:newTask' iconClass='plusIcon'>Add Task</div>
</div>
</div>


you can see, we've got some similarities to the previous template but we also have some new features we didn't see before. i'm going to concentrate on the differences. you can see that we defined the containerNode we needed for dijit._Container using dojoAttachPoint. this is where our Task widgets will be added to when we add them.

the main feature we've used here that we didn't use previously is dojoAttachEvent. this provides a convenient way for us to connect the function of a widget in our template to a function defined in our TaskList class. the way it works, is that the string before the ':' is the name of the button's function that will be connected to our widgets function - the name on the right of the ':'. the connection is made by using the widget's connect method and this is just a convenient way to shortcut the code to do that. you can see from our template that when the onClick of the button is called, we will execute the newTask function of our widget.

now let's look at the javascript code so we can see what it looks like

dojo.provide('my.widgets.TaskList');

dojo.require('dijit._Widget');
dojo.require('dijit._Container');
dojo.require('dijit._Templated');
dojo.require('dijit.form.Button');
dojo.require('my.widgets.Task');

dojo.declare('my.widgets.TaskList', [dijit._Widget, dijit._Templated, dijit._Container], {
templatePath: dojo.moduleUrl('my.widgets.templates', 'TaskList.html'),
widgetsInTemplate: true,

name:'',
_multiValue: true,

postCreate: function(){
if(!this.getChildren().length){
// add a default 'blank' task
this.newTask();
}
},

// this creates a 'blank' new task - defaults the time to now and the description is 'New Task'
newTask: function(){
this.addTask({
time: new Date(),
description: 'New Task'
});
},

// adds a single task to our list of tasks
addTask: function(task){
new my.widgets.Task({
value: task
}).placeAt(this);
},

// sets our value - takes an array of objects
// each object should have a time and a description property
_setValueAttr: function(value){
// if we already have a value then we need to clear the widgets that represent that value
// removes all the widgets inside our 'containerNode'
this.destroyDescendants();
if(dojo.isArray(value) && value.length){
dojo.forEach(value, this.addTask, this);
} else {
// default to a 'blank' task
this.newTask();
}
},

_getValueAttr: function(){
// this will return an array of the values returned from our children via child.attr('value')
return dojo.map(this.getChildren(), "return item.attr('value');");
},

addChild: function(child){
this.inherited(arguments);
var button = new dijit.form.Button({
label: 'Remove Task',
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconCancel',
onClick: dojo.hitch(this, function(){
this.removeChild(child);
button.destroy();
})
}).placeAt(child.domNode);
},

removeChild: function(){
this.inherited(arguments);
if(!this.getChildren().length){
this.newTask();
}
}
});


these blocks of code are getting bigger and bigger but that's ok. we've only added a few new concepts. most of it is familiar already. let's jump right past what's already familiar and take a look at the new properties we're adding to this widget (name, _multiValue). the name property needs to be defined so that it can be picked up by the parser when we add this widget to our form. properties defined in markup will only be added by the parser to the widget if the properties are in the prototype of the widget. so we have to define name so we can use it later. also, _multiValue is a property we need to set as true whenever we will be setting/getting an array as the value of this widget.

i'm just going to summarize quickly what each of the functions do:

postCreate


just adds an empty task if no value was passed in when the widget was created

newTask


this is the function called when the user clicks the button to add a task.

addTask


this function is used to add another task to our list.

_setValueAttr


removes any previous tasks, adds each task in the list, or adds a 'blank' task if the new list is empty

_getValueAttr


returns an array which holds the results of calling the .attr('value'); getter for each child.

addChild


we're extending this function to add a button to each of our children so that when the button is clicked, the child is removed and the button is also destroyed.

removeChild


we're extending this function to add a 'blank' task to the list if we remove the last child.

ok - take some time and digest that... we have 1 final piece and that's the form. this is what we've been trying to achieve from the start... and it's taken quite a while to get here. you can see why i didn't want to keep explaining this any more!

my.widgets.App


at the top-level, our data has 2 properties:

  • date - the date of our schedule

  • tasks - the list of tasks we have scheduled for this day



it's a long way to the top if you want to rock n roll but we've finally made it! well, you already know what we're trying to achieve so here's the code:

<div class='myApp'>
<form dojoType='dijit.form.Form' dojoAttachPoint='form'>
<h1>Daily Schedule</h1>
<input dojoType='dijit._Calendar' dojoAttachEvent='onChange:onDateChange' name='date' class='dijitInline' />
<input dojoType='my.widgets.TaskList' name='tasks' class='dijitInline'/>
</form>
</div>

this time, our template contains a form which contains a heading and 2 elements. we really haven't introduced any new concepts here so since this post is already a mammoth i'll leave it for you to read it and comprehend it.

and fortunately, most of the hard work has already been done so the javascript is just shown here to complete the code:

dojo.provide('my.widgets.App');

dojo.require('dijit.form.Form');
dojo.require('dijit._Calendar');
dojo.require('dijit.form.TextBox');
dojo.require('my.widgets.TaskList');

// a little hack to get dijit._Calendar to work with the parser.
// dijit.form.Form only takes values from widgets with names and
// the parser only sees properties in the markup which already
// exist in the prototype - hence...
dojo.extend(dijit._Calendar, {
name: ''
});

dojo.declare('my.widgets.App', [dijit._Widget, dijit._Templated], {
templatePath: dojo.moduleUrl('my.widgets.templates', 'App.html'),
widgetsInTemplate: true,

// this is just going to be something to connect to so we can know when the user selects a new date
onDateChange: function(value){}
});


there's probably only 2 things which need explaining here. the dojo.extend and the empty onDateChange function.

the dojo.extend is needed so that we can give the calendar a name property in the template. if you remember i said earlier:
properties defined in markup will only be added by the parser to the widget if the properties are in the prototype of the widget.

it turns out that dijit._Calendar doesn't have the name property defined because it wasn't intended to be used like this (until dojo 1.4).

and the onDateChange function is here so that we can connect to this function and do something based on when the user selects a different date. since this post is so long... that's another post for another day :)

hopefully i've helped you get inside the 'brain of dojo' and now you can see some of the patterns that have been designed into dojo and you can leverage these patterns to work with dojo rather than fight against it or re-invent the wheel.

thanks for staying through to the end.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Awesome blog. It was very helpful to me. Thank you.

    ReplyDelete
  3. This is pretty slick. Dijit is an extremely powerful library. Very nice blog. The Dojo 1.7+ version will be pretty similar and possibly a little cleaner, too. Good job.

    ReplyDelete