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, countrywidget.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 scheduletasks
- 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 beginsdescription
- 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 dojoAttachPoint
s 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 dojoAttachPoint
s 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 scheduletasks
- 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.
This comment has been removed by the author.
ReplyDeleteAwesome blog. It was very helpful to me. Thank you.
ReplyDeleteThis 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