The programming days of my work life are taken up by a major grants/accounts lifecycle Drupal application. This is a rewrite of a 10 year old Microsoft Access database+forms. It’s massive, it’s interesting, and I’m doing lots of things with Drupal that I have never done before, and some that until I got in to it I wasn’t even sure it was possible.
This week’s task (I get two days a week, usually less, to work on this) has been to implement defaults for some fields, based on a node reference. The main node type on this site is an ‘account,’ which is attached to a bunch of other node types via reference. One of these types is a ‘sponsor,’ basically a granting institution. These accounts include administrative, technical, and billing contact information. While these are per account, for a lot of sponsors they are per sponsor, meaning that they won’t change for each account that is attached to the same sponsor. But, it’s not always the case.
And it’s the “it’s not always the case” that has led to this task. Each sponsor will have default contact information, which should be copied in to the account. For many accounts, that’s fine. But for some, these contacts will have to be edited. So what we want is, when the user picks the sponsor (a nodereference) for the account (colloquially called a “card”), the contact information gets pre-filled from the defaults listed in the selected sponsor node.
I should add at this point that it’s very possible, likely, even, that I’m going about this in the wrong way. Please feel free to contact me and tell me how stupid I am and point me to the module that does this in a more elegant manner.
After looking at the #ahah properties that are part of FormAPI, I couldn’t figure out how best to adjust form values. The #ahah properties seem to be geared towards adding fields and manipulating divs by using wrappers and such. I’m sure there’s a way this could be shoehorned in to what I was trying to do, but I determined that it would just be too complicated and would feel too much like a hack.
Which leaves jQuery. jQuery’s part of Drupal, which has led to some great UI innovations and lots of cool functionality. And jQuery is opened up to Drupal in wonderful ways through the Drupal javascript object. I’m not a JavaScript or jQuery programmer, but I have dabbled a bit. So I decided that a jQuery-based solution would be the route I would take for this task.
First, here’s what the UI looks (just a placeholder theme in place at this point — don’t get excited):
The sponsor node where the “defaults” are put in:

And here’s the relevant portion of the card, where the pre-filling will take place:

So the card has a nodereference type pointing into the sponsors. There are multiple sponsors; the first sponsor is where the contacts will come from. The nodereference field is an autocomplete textfield that pops up a list of eligible sponsors and allows the user to pick one. The goal is that, once one has been picked, that the defaults would be looked up from that sponsor and the rest of the fields populated from that data.
The key part of this is getting the data out. If you ever develop for Drupal and find yourself with a task that can be stated like “What I need is to be able to construct a query…” your mind should immediately turn to Views.
So much has been written about the incredible Views module, the versatile query builder from super-smart nice guy Earl Miles (@merlinofchaos). I’m not going to go in to a discussion of what Views is here, except to say that Drupal without Views is little more than a well constructed blogging platform. With Views, it’s a powerful tool for quickly writing versatile web applications.
Views could build the query for me, of course. It’s a very simple view in fact: “Gimme these fields from a sponsor node with nid of x.” A few fields, and an argument for nid. But how could jQuery handle this data?
Everyone that said “JSON” gets a point. Part of what makes Views so completely awesome is the system of plugins that orbits around it. Views can be adapted with other code to output in a variety of formats. The “views_datasource” suite of modules allows Views to output in a variety of microformats, including XML, RDF, and, yes, JSON. A views “page” that is available at a URL and outputs JSON is exactly what jQuery would like to work with, and perfectly fits this use case.
So all things are now ready: We’ve got the nodes and fields defined, a view to get the data from one node, and a clear idea of the task. All that’s left is to write the code.
The module for this is exceedingly simple. All we really need the PHP to do is to add the Javascript to the appropriate form. This is just a hook_form_alter(), calling drupal_add_js(). The meat of the module, then, is just:
function cards_ahah_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'account_node_form') { // here we are
drupal_add_js(drupal_get_path('module', 'cards_ahah') .'/sponsorajaxhelper.js');
}
} // endfunction cards_ahah_form_alter
Yeah, really, that’s it. Yes, I’m calling the module “cards_ahah” — that’s just historical, from earlier attempts, but it sort of fits. So the bulk of the work will be in the referenced Javascript file, sponsorajaxhelper.js, which will be attached to only the edit form for new accounts (cards).
The tasks the Javascript will need to complete are:
value of each element to the appropriate value from the JSON.Step two is easily handled by a call to jQuery.getJSON(). The third by a series of calls to $().attr(). The first requires just a touch more work.
While we can get the value of the field by calling $().attr('value'), it’s not the nid that we need for the view. That is, it’s not only the nid. It actually looks like “Sponsor [nid:44].” The nid is in there, it’s just surrounded.
Stand back, I know regular expressions! So a call to $().attr('value').match() can grab it.
Attach that to $().blur(), and we’ve got our solution. Here, then, is what the jQuery code looks like:
Drupal.behaviors.sponsorhelper = function () {
$("input[name='field_sponsor[0][nid][nid]']")
.blur(function() {
nidRegEx = /\[nid:(\d+)\]/;
SponsorHelper.fill($(this).attr('value').match(nidRegEx)[1]);
})
};
SponsorHelper.fill = function(nid) {
var url = Drupal.settings.basePath +
'sponsorajaxhelper/' + nid;
jQuery.getJSON(url, function (data, result) {
if (result != 'success') {
return;
}
$("input[name='field_admincontact[0][value]']").attr('value',data.nodes[0].node.field_admincontact_value);
$("input[name='field_adminphone[0][value]']").attr('value',data.nodes[0].node.field_adminphone_value);
(and a lot more of these, filling in the fields)
So, let’s go through that script in a little more detail.
Our call to Drupal.behaviors.sponsorhelper sets up our helper function. In our case, we’re attaching the SponsorHelper.fill() function to the blur event on the first sponsor’s text field. Which is to say, when the user clicks away from the sponsor’s field, we go. We need to pass the nid to the function, which is what the nidRegEx does, along with the call to $().attr('value').match()[1], grabbing the digits out of the regex.
Our SponsorHelper.fill() function first sets up the url that we’ll go to to get the JSON data. This is the page that the view exposes, ‘sponsorajaxhelper plus the nid. The call to jQuery.getJSON() runs the view, with the nid argument, as specified in the URL. What we get back, if we get anything back, will be a “nodes” object inside the data variable.
From there, it’s just a bunch of calls to $().attr() to set the other fields based on the returned data.
What could be simpler? (rhetorical)
This was actually a very interesting task. I’d not messed with jQuery very much, and certainly not tried to integrate it within Drupal to the point this requires. It ends up working very well — I can have defaults stored in the sponsor node, they get copied but are yet fully editable in the account node, where they’re properly stored. Sponsors where the defaults are good enough are completely filled in by simply selecting the sponsor. Those that need editing have sensible defaults.
Again, you Drupalers that are reading this have probably thrown up a few times during this discussion, and can’t believe that I’m doing this in such an incredibly wrong way. Again, please contact me and set me straight. I’ve got a few hours invested in learning this, but I’m very eager to see that this is horribly wrong and that there are lots of better ways to do this.