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

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

(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

Comments

Amazing!

joshk's picture

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's picture

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's picture

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

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's picture

Glad you find it useful!

Victor Kane
http://awebfactory.com.ar

Looks great victor

newdru's picture

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

mrgoltra's picture

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

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!

-VVvvvvvvv v v

This IS actually our discussion forum

Senpai's picture

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]


Joel Farris | my 'certified to rock' score
Transparatech
http://transparatech.com
619.717.2805

Is there more information

joetsuihk's picture

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's picture

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's picture

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's picture

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's picture

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's picture

...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!

-- Steven N. Severinghaus <sns@severinghaus.org>

I am trying to use the above

mzafer's picture

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

Event Order?

raintonr's picture

Thanks for posting this - it has explained a bit about Jquery, but brought up a few more things.

First off, I'm using D5.9, the version of autocomplete.js I have says this at the top:

// $Id: autocomplete.js,v 1.17 2007/01/09 07:31:04 drumm Exp $

Am trying to do a similar thing - sort of. I want to perform a lookup based on an auto competed value and work with that result.

Trouble is that when I follow your lead by adding a blur event handler to the source input (the one that is auto completing) it seems to be triggered before autocomplete.js has done it's job.

Ah... I discover this varies on the method one uses to select from the auto complete drop down! Use the mouse == good, use the keyboard == bad!

Ie: Type something in the source auto complete field. Get a drop down. Select one of the values by using the up/down arrows and tab. The auto complete field changes when you press tab. The custom blur handler is triggered at this point but grabbing $(this).val(); in there gives just the few letters that were typed, not the full text that is now visible in the source field.

But: Use the mouse and select one of the values that way and $(this).val(); in the blur handler does return the full auto completed source value.

This is the case whether adding your custom function to change or blur events.

I can't figure out why using the mouse helps. Nor why when change is used even the handler isn't triggered multiple times, or at least the last time should be when the autocomplete.js fills in the full value.

Any ideas? Is this a potential bug in autocomplete.js?

Timer Kludge

raintonr's picture

Well, this is a horrible kludge, but it works... just set a timer in your own function that waits a while (300ms seems to work) so auto complete can finish it's stuff first:

if (Drupal.jsEnabled) {
  $(document).ready(function() {
    $("#your-field-id").blur(function() {
      this.timer = setTimeout(function() {
        /* Do your stuff here, after waiting for auto complete - Yerch!*/
      }, 300);
    })
  })
}

Yerch! There must be a better way?

Use focus

shepherd's picture

I found that using the destination fields focus event worked better than blur.

Also, consider unbinding all input fields. It appears that Drupal5 keeps adding more duplicate bindings. Something like:

$('input').unbind();
$('input......').focus(setAutoComplete)
Drupal.autocompleteAutoAttach();

Here is my working js code that fixes all the issues that I found:

if (Drupal.jsEnabled) { $(document).ready(function() {

Drupal.it_call_log = Drupal.it_call_log || {};
Drupal.url = '';

Drupal.it_call_log.clientAttach = function(url) {
  Drupal.url = url;

  // unbind everything in sight so we don't get duplicate bindings.  Drupal.autocompleteAutoAttach reattaches everything, even if already attached.
  // Would be better to only unbind those with associated autocomplete fields.
  $('input').unbind();
  $('#edit-summary').focus(Drupal.it_call_log.setAutoComplete);
  $('input[@id^=edit-caller-name]').focus(Drupal.it_call_log.setAutoComplete);

  Drupal.autocompleteAutoAttach();
}

Drupal.it_call_log.setAutoComplete = function() {
  client_name = $('#edit-client-name').attr('value');
  name = $(this).attr('name');
  str = "edit-" + name + "-autocomplete";
  autocomplete = str.replace(//g,"-");
  type = name.replace(/
\d+$/,"");

  url = Drupal.url + type + '/' + client_name;
  $('#'+autocomplete).val(url);

  // $('#edit-debug').val("x"+url);

  Drupal.it_call_log.clientAttach(Drupal.url);
};

});}

php code:

  drupal_add_js(drupal_get_path('module', 'it_call_log') . '/it_call_log.js');
  drupal_add_js( '$(document).ready(function() {
    Drupal.it_call_log.clientAttach("'.$url.'");
});',"inline");

Also found a bug in the down arrow that is fixed with the following patch: (I can't find the original patch reference)
--- autocomplete.js     (revision 1493)
+++ autocomplete.js     (working copy)
@@ -109,6 +109,10 @@
  * Highlights the next suggestion
  */
Drupal.jsAC.prototype.selectDown = function () {
+  if (this.popup == null) {
+    return false;
+  }
+
   if (this.selected && this.selected.nextSibling) {
     this.highlight(this.selected.nextSibling);
   }
@@ -280,9 +284,6 @@
           }
           db.owner.setStatus('found');
         }
-      },
-      error: function (xmlhttp) {
-        alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ db.uri);
       }
     });
   }, this.delay);

with() great, unbind() too much

bokabu's picture

These comments were very helpful! I'm now using (on Drupal 5.x) the with(...) {...} to remove, because unbind() also took out my own custom onchange() event.

Excellent code sample

Kristen Pol's picture

Thanks for this great code sample. I will put it to use this week and will give you feedback on whether I run into any issues.

blur vs. change

Kristen Pol's picture

This may have been noted above but, if not, you need to make sure to use blur vs. change event. If you use change, the value you get is only the part that was typed in by the user and not the whole autocompleted text.

Apologies for brining this back from the dead

steveadamo's picture

I have a somewhat similar autocomplete issue I'm trying to resolve. The full details can be found on a post on Drupal.org here:

http://drupal.org/node/443056

Effectively, I'm trying to provide additional details in my autocomplete field (to provide a means for my users to distinguish between similar results). In my case, instead of the autocomplete just returning the Name of my content type, I would like it to provide some location fields as well (street, maybe city,m etc.).

The user might type in the name of a Center like "kid" and not only get the match for all centers starting with "kid", but have those names appended with the street locations of those Centers.

Kids n Action - 123 Smith St.
Kids n Action - 456 Jones St.

Thanks for any insight. My eyes are bleeding from all the "googlin". :)

Use a View

aaron's picture

You can do that with a view: use a view for the 'selector' (as specified when setting up the nodereference field), and then use 'name' and 'address' for the fields (which will be printed during the autocomplete process).

Aaron Winborn
Drupal Multimedia (book, in October!)
AaronWinborn.com (blog)
Advomatic (work)

Aaron Winborn
Drupal Multimedia (my book, available now!)
AaronWinborn.com
Advomatic

Thanks Aaron

steveadamo's picture

I really appreciate the response. I've become a Views junkie, so I'm surprised this idea hadn’t dawned on me! :)

Just to clarify though, I have an existing custom content type (and associated form) "Group Event", that has an autocomplete field for the Event location (the Center). Could you elaborate on how I would integrate my new view with the existing form?

Although I have a sneaking suspicion you weren’t suggesting that.

If you prefer, I can take this conversation out of this thread.

Edit #2: and after re-reading your response, I think it sunk in. I blame the lack of late afternoon caffeine. If you dont mind, i'll give this a shot on my local instance, and report my results here?

Half way there!

steveadamo's picture

Thanks Aaron, your suggestion worked like a charm!

The only problem is the end result, once a selection is made. The text field is populated with the name, followed by the nid (the extra fields are removed).

Kids R Kids [nid:17]

Is there any way I can display the original entire selection (Kids R Kids - 123 Smith St. - Houston), or at a minimum trim that last part?

For those that are curious, the exact steps to implement this were:

  1. Create a View for the content you are trying to display (in my case Node Title - Street - City)
  2. View your custom content type
  3. Click Manage Fields
  4. Add a new field - node reference - autocomplete text field
  5. On the config screen for the new field, scroll to the bottom
  6. Open Advanced - Nodes that can be referenced (View)
  7. Click the View used to select the nodes: dropdown and select your View from step 1
  8. Save and test! :)

edit:
is this the best (and only) solution?
http://drupal.org/node/365241

edit #2:
looks like i found my answer... :)
http://drupal.org/node/234449

GMap In View

eradeepak's picture

GMap Not dispaly in View module. I need to dispay the gmap in view. how can i dispaly.
pls give me replay.

Is there a d6 solution? I

DanChadwick's picture

Is there a d6 solution? I seem to be thwarted by a lack of deep jQuery knowledge and closures.

DanChadwick's picture

My use case has two needs. The first is that some autocomplete fields depend upon the current value of another field (which is itself also autocomplete). The second is that these fields needs to perform an initial autocomplete lookup, even if the field is entry (unless both fields are empty).

The strategy was:
1) Theme these special autocomplete fields with a special class so that the javascript can find them.
2) Open drupal6/misc/autocomplete.js. Copy Drupal.behaviors.autocomplete to a new .js file. Use drupal_add_js to add this file when needed.
3) Rename the copy of the function to Drupal.behavior.XXXXX and update the loop to look for the new class. At the end, add the class XXXXX-processed, in addition to autocomplete-processed.
4) In the loop for each special autocomplete field, find the associated field upon which the autocomplete for this field depends.
5) $(input).unbind(); // to remove any bindings from the regular autocomplete code. In my case, my behavior ran first, but this isn't guaranteed.
6) Create the jsAC object and, with a closure, make whatever changes you need to it. I used:

    (function(ac) {
      ac.main = main;                             // Save associated field for use by handlers
      ac.populatePopup = XXXXPopulatePopup;   // Replace methods with customized versions
      ac.found = XXXXlFound;
      ac.onkeyup = XXXXOnKeyUp;
      $(input)
        .focus(function () {                      // Add focus handler
          if (main.value.length > 0 || input.value.length > 0) {
            ac.populatePopup();
          }
        })
        .unbind('keyup')                          // Unbind keyup handler and replace with new one
        .keyup(function (event) { ac.onkeyup(this, event, main); })
    })(new Drupal.jsAC(input, acdb));

7) Write the special methods above. For my use case:
7a) XXXXPopuplatePopup concatenates the associated field with this field using an underscore, such as AssociatedCategory_ThisFieldValue. Of course, the PHP menu callback for the the autocomplete needs to be written to expect this underscore format. For my application, underscore was an illegal character; you might need to escape a character.
7b) XXXXOnKeyUp. Make tab return true so that it doesn't immediately hide the popup. Populate the popup when either of the two fields has a value.
7c) XXXXFound: Don't return immediately unless both fields are empty.

I hope this roadmap helps someone else.

Sorry to pop in on such a

Nolza's picture

Sorry to pop in on such a long thread... but does anyone have any ideas on a possible Drupal 7 iteration of the ideas mentioned above?