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

You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!
public

(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

Amazing!

joshk's picture
joshk - Tue, 2007-04-03 13:37

Victor, this fantastic. I have to review in more detail later, but I'm a big fan of this format.

We should find a place in the drupal.org handbook to place this.

http://www.chapterthreellc.com | http://www.outlandishjosh.com


Thanks, Josh

victorkane@drupal.org's picture
victorkane@drup... - Tue, 2007-04-03 15:01

I am hoping some of the javascript gurus will take a look at some of the difficulties I was experiencing with autocomplete (couldn't figure out how to navigate existing jsAC objects to eliminate old handlers because I think there is no container). So hopefully there will be some discussion on this in the javascript group.

As to the format, I think I have hit upon a way to make available useful stuff one does on a concrete project which, while not being at the module level yet (too many business case dependencies) still could be useful.

So I will repeat it in the future, and sure hope others do!

Victor Kane
http://awebfactory.com.ar


Looks very useful!

ximo@drupal.org's picture
ximo@drupal.org - Tue, 2007-04-03 15:13

I like how clean it looks (in code).. Will definetely check it out when I get the time, and report any findings or other uses for it back to this post :)

And I agree that it's a good thing to document any useful tips or methods used on different projects back to the community like this, I think there's a lot of creative solutions that never makes it to the handbook for example.


nice one

moshe weitzman's picture
moshe weitzman - Tue, 2007-04-03 14:19

i have been wanting to offer an autocomplete callback for all members of an organic group and now i know how. thanks!


Thanks, Moshe!

victorkane@drupal.org's picture
victorkane@drup... - Tue, 2007-04-03 14:59

Glad you find it useful!

Victor Kane
http://awebfactory.com.ar


Looks great victor

newdru@drupal.org - Fri, 2007-04-06 09:25

I haven't tried this myself yet but it looks like this could work with a similar scenario using:

Country -> Region (state) -> City -> Venue

meaning country drills down to region.. then region drills down to city..
then when you drilled down to the city, any venue node that node referenced on that city would be populated in the dropdown.

Can that be done with your methodology?

Also, since location module can nodeapi the location data on any node type (venue), how do you link the fields up using your technique above?

Any thoughts much appreciated

thanks

do you mean

mgoltra's picture
mgoltra - Sat, 2007-04-14 08:31

for example

Country > Region/State > City

USA > will yield only US region or state available on dropdown > then only cities within that state?

I have been looking for something like this to implement on my test site. Is there such a module, group or instructions to do this. Any additional info on this anywhere on the forum.. The way I have been doing it is manually entering the country then that's it.

TIA

Mark


hello

drozzy's picture
drozzy - Fri, 2007-04-06 14:57

Hello everybody, sorry for out of place question:
I am new to drupal and drupal dojo...
I clicked on a "discussion" link on drupal dojo website and got here. But i don't see any forums...
This drupal seems very confusing. How do these wiki-forums work? I mean how do i start a new topic or something?...

Thanks in any case!


This IS actually our discussion forum

Senpai's picture
Senpai - Mon, 2007-06-11 17:51

Hi drozzy, and welcome to the dojo! This is our discussion forum. Yes, you can create new threads on it, and yes, you can comment on other's threads if they've turned on comments for it. There is only one forum for our use, and it's part of an Organic Group called 'drupal-dojo'. There are many groups here on g.d.o, and each one has it's own forum for users of that group.

The cool thing about this setup is that you can Create a New Story that shows up in every group that's it's relevant to, provided you are also a member of that group(s). See?
[/Senpai]


Is there more information

joetsuihk's picture
joetsuihk - Tue, 2007-04-10 07:16

Is there more information about step3,
Drupal 5.x style with views as an extra data abstraction layer, none of your direct database accessing?

interested, but google finds nth...


Getting rid of caveats

ubul - Mon, 2007-06-11 12:48

I have debugged autocomplete.js a bit, and managed to find a solution for the aforemention caveat, that old handlers are remaining to distract autocomplete.

The solution below is specific. Generalize it as you need. The accent is on the with(){} block.

if (Drupal.jsEnabled) {
    $(document).ready(function() {
      $("#edit-company").blur(function() {
        company = $("#edit-intezmeny").attr("value");
        $('#edit-dependent_field-autocomplete').attr('value', "?q=worksheet/machine_autocomplete/" + company);
        with (document.getElementById('edit-dependent_field')) {
            for (i in events.keyup) delete events.keyup[i];
            for (i in events.blur) delete events.blur[i];
            for (i in events.keydown) delete events.keydown[i];
        }
        Drupal.autocompleteAutoAttach();
        });
   })
}

right, very interesting!

victorkane@drupal.org's picture
victorkane@drup... - Fri, 2007-06-22 23:11

That's exercising good DOM sense.
Yes, it will be interesting to generalize this and offer up a patch...

Victor Kane
http://awebfactory.com.ar


Trying to get this to work in Drupal HEAD

patrickharris@d... - Tue, 2007-08-07 07:28

I got this working in 5.1, but not HEAD. I think in Drupal 6, Drupal.behaviors.autocomplete() is used instead of Drupal.autocompleteAutoAttach();. Also, I'm getting an error that 'events is not defined', so I guess the code:

with (document.getElementById('edit-dependent_field')) {
for (i in events.keyup) delete events.keyup[i];
for (i in events.blur) delete events.blur[i];
for (i in events.keydown) delete events.keydown[i];
}

needs to change. Can anyone help out here? Has anyone got this going with Drupal HEAD/6?

This should work

debdungeon@drup... - Tue, 2008-06-10 17:29

Replacing the above code with this seems to solve the multiple handler problem.

  $("#edit-dependent_field").unbind();

As unbind() removes all bound events.

Minor jQuery style point

HorsePunchKid@drupal.org's picture
HorsePunchKid@d... - Mon, 2007-09-03 22:14

...and I haven't tested it completely yet. But I think the code:

3      $("#edit-field-company-0-node-name").blur(function() {
4        companyvalue = $("#edit-field-company-0-node-name").attr("value");
...
12     })

...is better written in the jQuery idiom as:

3      $("#edit-field-company-0-node-name").blur(function() {
4        companyvalue = $(this).val();
...
12     });

Like I said, very minor. Great lesson, though; perfect for what I'm trying to accomplish!


I am trying to use the above

mzafer@drupal.org - Mon, 2008-02-04 03:46

I am trying to use the above logic to integrate with CCK_Address module to auto complete the city based on the state. After some wierd javascript behaviour.. i am almost close. The issue now is that mutliple handlers trigger the autocomplete, display overlapping autocomplete list. Infact a handler is added everytime I select a state. So far I am unable to remove the handlers, any thoughts on this will be appreciated.

And thanks for the wonderful article and suggestions.

-zm