Quantcast
Channel: Recent posts across whole site
Viewing all articles
Browse latest Browse all 49197

Practical lesson: An autocomplete nodereference field that is dependent upon a choice made in another

$
0
0

(Introducing the shiny new "Dojo Practical Lesson" format)

Problem statement

If you need to do this, feel free to use this code. If you can think of ways of improving it, please join in.

So I have three content types with additional fields added through CCK.

Content type one: Company

Content type two: Contact

Content type three: Ticket

The Company content type has a nodereference field where Contacts can be filled in (multiple autocomplete field).

Among other fields, the Ticket content type has a select field "type" (phone call, sale, task, bug, hot date...); a single non-multiple nodereference field for Company; and multiple nodereference fields for Contacts involved in the ticket.

Since this has been done for a company that has at present over three thousand contacts distributed among five hundred companies, the chance of people with the same name but working in different companies (and actually being different individuals) is high (muchos María Gonzalez y Henry Smith). So... is Drupal a web application or not?

Background Material (alas for Drupal 4.7 only...)

Tutorial 2: Using existing Javascript widgets: autocomplete.

Asked and answered: altering autocomplete forms with javascript after they're rendered.

OSCMS 2007 buzz, various..., including Steve Witten's jQuery slides...

Solution (in true Drupal 5.x style, with Views and jQuery!)

As the above "Tutorial 2" explains, the anatomy of any autocomplete function has three fundamental components:

  1. A handler (a php function which will do the actual looking up).
  2. A MENU_CALLBACK path (URI) which will be invoked by the autocomplete query to access the "handler".
  3. Include the special #autocomplete_path selector in a form field.

In my above example, my three or four Contact nodereference autocomplete fields by default already have all this, but the default handler (which lives in the entrails of CCK's nodereference.module,
specifically the nodereference_autocomplete() function registered by nodereference_menu() and referenced in the nodereference_widget()
function) will unwittingly bring me all Jane Doe's and María Bustamante's whether they work for the already selected company or not.

Step 1: MENU_CALLBACK uri setup

Question: But where does the code go?

Answer: All the code goes into a module, call it intranet.module (create the appropriate intranet.info file -- see first couple of Dojo lessons if you are not sure how). The only exception is the javascript code, which goes into intranet.js in the module directory. I put my modules, into ./sites/all/modules/, so the three files would go into ./sites/all/modules/intranet. You can, of course, name the module whatever you want. Just be sure the drupal_add_js function properly invokes the javascript file.

/**
* Implementation of hook_menu().
*/
function intranet_menu($may_cache) {
$items = array();
$items[] = array(
'path' => 'intranet/ticket/contact/autocomplete',
'title' => 'contact autocomplete for ticket',
'type' => MENU_CALLBACK,
'callback' => 'intranet_autocomplete',
'access' => user_access('access content'),
);
return $items;
}

All we are saying here is that the path 'intranet/ticket/contact/autocomplete'
will invoke the function 'intranet_autocomplete',
the "handler".

Step 2: Attach the overriding uri to the Contact autocomplete field with inobtrusive jQuery

function intranet_form_alter($form_id, & $form) {
if ($form['type']['#value'] == 'ticket') {
$form['#theme'] = 'intra_ticket';
}
}
function theme_intra_ticket($form) {
drupal_add_js(drupal_get_path('module', 'intranet') . '/intranet.js');
}

And intranet.js (short and sweet):

1  if (Drupal.jsEnabled) {
2 $(document).ready(function() {
3 $("#edit-field-company-0-node-name").blur(function() {
4 companyvalue = $("#edit-field-company-0-node-name").attr("value");
5 re = /[nid:(.*)]/;
6 resarray = re.exec(companyvalue);
7 if (resarray) company = resarray[1];
8 $('input.autocomplete[@id^=edit-field-contact-]').each(function () {
9 this.value ="http://mentor/intrassa/intranet/ticket/contact/autocomplete/" + company;
10 Drupal.autocompleteAutoAttach();
11 });
12 })
13 })
14 }

OK, explanations are in order:

On line 3 we assign an anonymous function to the blur event of the company autocomplete field. The idea is that the user selects a company,
and then this anonymous function gets invoked. This anonymous function will first extract the node id of the selected company via javascript's regular expression tools.

Then, moving right along, on line 8, another anonymous function will iterate over the multiple contact fileds (those that start with an "id" attribute of "edit-field-contact-") and will shove in the new handler uri (TODO: here horribly hard coded, but which should actually be passed to javascript as a drupal_add_js invoked inline var sequence), to which is tacked on the company node id as parameter to the handler.

Then, on line 10, Drupal.autocompleteAutoAttach() is invoked to fix the autocomplete "community plumbing" (see ./misc/autocomplete.js).

All that is left is the handler itself (or so I thought myself: see below, however, for an extra bit of fun).

Step 3: Handler (Drupal 5.x style with views as an extra data abstraction layer, none of your direct database accessing)

Here it is:

/**
*
*/
function intranet_autocomplete($company) {
$view = views_get_view('ContactsForASingleCompany');
$the_contacts = views_build_view('items', $view, array(0 => $company), false, false);
$matches = array();
foreach ($the_contacts['items'] as $contact) {
$matches[$contact->node_title . ' [nid:' . $contact->nid . ']'] = $contact->node_title;
}
print drupal_to_js($matches);
exit();
}

The tacked on $company appears to us like Venus from the Sea as a parameter, and we use it to satisfy the argument of the pre-created View concocted with the views module for another purpose and cunningly reused here. We iterate over the contacts the built view brings us, and set up the $matches array, which we translate to JSON before exiting (which is what autocomplete expects).

Caveats

First caveat is (and I knew this was going to happen, due to having read the piteous cries of wanderingstan in his recent post at the foot of one of the background posts): "The problem is that the old handlers are still there. So you end up with 2 GET requests, one to the old handler and one to the new."

My solution for this: cast the offending "old handler" and its results into the bit bucket. A more elegant solution ("")
has been hampered so far by my being unable to address the appropriate Drupal.jsAC (javascript autocomplete) objects and null-ify them or unset them, as Stan says: "I'm working on trying to wipe out all existing autocomplete events/objects before calling autocompleteAutoAttach() which will build them fresh again." But I don't want to do away with all, since I have a lot of other autocomplete stuff on my page I want to leave alone; although I would like to do away with the "old" overriden handlers which were created on page load.

My solution/hack for banishing the offending handlers: redefine their callback function specified by nodereference.module to a special bitbucket function I have. See the following three functions:

/
* Implementation of hook_menu().
*/
function intranet_menu($may_cache) {
$items = array();
$items[] = array(
'path' => 'intranet/ticket/contact/autocomplete',
'title' => 'contact autocomplete for ticket',
'type' => MENU_CALLBACK,
'callback' => 'intranet_autocomplete',
'access' => user_access('access content'),
);
$items[] = array(
'path' => 'nodereference/autocomplete/field_contact,
'title' => 'test',
'type' => MENU_CALLBACK,
'callback' => 'intranet_bitbucket',
'access' => user_access('access content'),
);
return $items;
}
/
*
*/
function intranet_autocomplete($company) {
$view = views_get_view('ContactsForASingleCompany');
$the_contacts = views_build_view('items', $view, array(0 => $company), false, false);
$matches = array();
foreach ($the_contacts['items'] as $contact) {
$matches[$contact->node_title . ' [nid:' . $contact->nid . ']'] = $contact->node_title;
}
print drupal_to_js($matches);
exit();
}

/**
* No other way to render inocuous the URI attached to autocomplete fields by page load.
* You have to find out what they are in firebug and redefine the MENU_CALLBACK functions
*/
function intranet_bitbucket() {
$matches = array();
print drupal_to_js($matches);
exit();
}

Another caveat is that my handler doesn't use the second parameter (which is the value entered by the user in the contact autoreference field) because in my case at hand, whatever the user enters, the javascript pushes in the company node id, and I don't care what the user put in on the contact field for this particular application; if you need that more normal autoreference behavior, simply accept the second argument and make sure you can use it as a second argument matching the view you are building there (or as part of a direct database query). For good examples of database queries (using db_rewrite_sql) see the nodereference.module (part of CCK).

Please let me know if this is useful, and please shower me with your corrections and suggestions.

And I would encourage my colleagues to continue using this "Practical lesson" format: it is so useful for all concerned to be able to share stuff we have needed for concrete projects,
isn't it?

saludos,

Victor Kane
http://awebfactory.com.ar


Viewing all articles
Browse latest Browse all 49197

Trending Articles