background image

Content tagged with: callback

Eric's picture

If you're like me, you probably use complicated node types with many fields and custom layouts. If that's the case, the default printer-friendly rendering of nodes may not suit you. In this code snippet I'll explain how you can override the templates and how I got to this step.

The first step when making modifications like this is to determine how the html was generated (template, function, callback, etc). Take a look at the query string for a printer friendly page: book/export/html/YOURNODEID. If you check out the book module, you'll see an entry in the book_menu() hook (book/export/%/%) that uses the book_export() page callback. Examining that function leads you to book_export_html(). This function generates the html for printer-friendly layout using the book_export_traverse() function and then calls the theme() function. The theme function allows us to override the theme layer. In this example I'll create a function that follows the templates suggestions guidelines to insert my code...

<?php
// the first argument being passed to theme function is book_export_html, so I'll prefix my function with my theme's name
function MYTHEME_book_export_html($title, $contents, $depth) {
 
// since the node ID is not being passed as an argument,
  // we'll grab it from the query string...
  // load node
 
$node = node_load(arg(3));
   
 
// define a list of node types to override
 
$nodeTypes = array('MYCOMPLICATEDNODETYPE1','MYCOMPLICATEDNODETYPE2');
  if (!
in_array($node->type, $nodeTypes)) {
   
// for all the rest of my node types, I'll use the default template:
   
return theme('MYTHEME_book_export_html', $title, $contents, $depth);
  }

 
// now, you have access to the $node variable and can do what ever you'd like with it
  // note: the drupal_render() function is great for generating the output for a CCK field

 
return "MY NEW AMAZING PRINTER-FRIENDLY HTML";
}
?>

In the above code, the theme registry was overrode by creating a function named: MYTHEME_book_export_html. The last part of this code snippet is returning regular nodes back to the default book template file (book-export-html.tpl.php). In the above code, I mention a theme registry entry called "MYTHEME_book_export_html", which has not been registered yet. We can register a new template using hook_theme()...

<?php
function MYTHEME_theme() {
  return array(
   
// register the default book export template
   
'MYTHEME_book_export_html' => array(
     
// pass it the same arguments, defined in the book_theme() function
     
'arguments' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL),
     
// define the same template as defined in the book_theme() function
     
'template' => 'book-export-html',
     
// since this file does not exist in our theme directory,
      // we'll specify the path of the book module
     
'path' => 'modules/book',
    )
  );
}
?>

Now that we've registered the "new" template (and rebuilt the theme registry) all of our standard nodes will use the original book template file and our custom node types will be modified as defined.

Eric's picture

This code snippet shows you how to refresh a view using AJAX and form controls, and allows users to save their search parameters as nodes. This tutorial consists of 2 major parts: 1) regenerating a view using arguments; and 2) saving these arguments in a node which can be reloaded at a later point.

1) Create a multiple select taxonomy vocabulary and add some terms to it. For my example, I called the vocabulary "Eric's Vocab 1" and assigned 5 terms to it: Term 1, Term 2, Term 3, Term 4, Term 5. It will be used to pass term IDs as arguments to the view.

2) Create a view that has a "Taxonomy: Term ID" argument. Make sure you choose "Display all values" for the action to take if argument is not present. Add a validator for Taxonomy term. Choose "Term IDs separated by , or +" for argument type. Enable allow multiple terms per argument and allow multiple arguments to work together. Add view filters and fields as necessary. For my example, I created a view called "eric_ajax_reload", added filter for published nodes, and added the node title and taxonomy terms as fields.

3) To test my view and argument, I added a bunch of content that had various taxonomy terms assigned to each one. I used the Live Preview functionality at the bottom of the view-edit screen to ensure my view was working with arguments.

4) Create a menu callback to embed the view into a page and bypass the theme layer:

<?php
// define menu item
function ericview_menu() {
 
$items[] = array();

 
$items['ericview/page/view'] = array(
   
'page callback' => 'ericview_callback_page_view',
   
'access arguments' => array('access content'),
   
'title' => t("Eric's Title"),
   
'type' => MENU_CALLBACK,
  );
   
  return
$items;
}

// define menu callback
function ericview_callback_page_view() {
 
$page_content = "";
 
$page_contents .= _ericview_view_create();
  print
$page_contents
 
exit();
}

// define function to create view html
function _ericview_view_create() {

 
// TODO: validate $_GET args
   
  // create view arguments
 
if (is_array($_GET['vocab1'])) {
   
$selectArgs = implode('+', $_GET['vocab1']);
  }

 
$html = "";
 
$viewName = 'eric_ajax_reload';
 
$display_id = 'default';
 
$html .= views_embed_view($viewName, $display_id, $selectArgs);
  return
$html;
}
?>

Now, if you flush your menu cache, you should be able to get to this new page. At this point, modifying the vocab1 query string value and reloading the page should update your view. If not, make sure your argument is working properly using the Live Preview.

5) Create another page callback that will consist of 2 columns: 1 column that has a form used to update the view, and 1 column that shows the view.

<?php
// define menu item
function ericview_menu() {
 
$items[] = array();
 
// ...code...
 
$items['ericview/page'] = array(
   
'page callback' => 'ericview_callback_page',
   
'access arguments' => array('access content'),
   
'title' => t("Eric's Title"),
   
'type' => MENU_CALLBACK,
  );
 
// ...code... 
 
return $items;
}

// define menu callback
function ericview_callback_page() {
   
 
// include css
 
drupal_add_css(drupal_get_path('module','ericview') . '/ericview.css');

 
// include javascript
 
drupal_add_js(drupal_get_path('module','ericview') . '/ericview.js');
   
 
$page_contents = "";
 
$page_contents .= "<div id='eric_column_left'>"
   
. drupal_get_form('_ericview_form') . "</div>";
 
$page_contents .= "<div id='eric_column_right'>"
   
. _ericview_view_create() . "</div>";
 
$page_contents .= "<div class='clearBoth'></div>";
   
  return
$page_contents;
   
}

// create a form that uses the taxonomy vocab as a multiple select element
function _ericview_form() {

 
$form = array();
   
 
$vocabName = "Eric's Vocab 1";
   
 
// TODO: use a function instead :)
 
$sql = "
    select td.tid, td.name
    from {vocabulary} v
    join {term_data} td on td.vid = v.vid
    where v.name = '%s'"
;
 
$resource = db_query($sql, db_escape_string($vocabName));
 
$results = array();
  while (
$row = db_fetch_array($resource)) $results[$row['tid']] = $row['name'];
   
 
$form['vocab1'] = array(
   
'#title' => $vocabName,
   
'#type' => 'select',
   
'#options' => $results,
   
'#multiple' => true,
   
'#attributes' => array(
     
'onChange' => 'ericview_select_change(this);',
    ),
   
'#default_value' => is_array($_GET['vocab1']) ? $_GET['vocab1'] : array(),
  );
   
 
$form['save'] = array(
   
'#type' => 'submit',
   
'#value' => 'Save',
  );
   
  return
$form;
  
}
?>

Here is the jQuery I added to get the selected options from the select element, make the AJAX call to regenerate the view, and update the contents of the right column

<?php
function ericview_select_change(whichThis) {
  var
vocab1Options = new Array();
  $(
'option:selected', whichThis).each(function(i){
   
vocab1Options[i] = $(this).val();
  });
   
 
// define args
  // note: the following will not work
  // var args = { 'vocab1': vocab1Options };
  // generates: ?vocab1=2&vocab1=3&vocab1=4
  // not:       ?vocab1[]=2&vocab1[]=3&vocab1[]=4
 
var args = { };

 
// create get path       
 
var getPath = '/ericview/page/view'   

 
// add query string arguments to get path
 
getArgs = "";
  for (var
i=1; i<=vocab1Options.length; i++) {
    if (
getArgs.length==0) getArgs += "?";
   
getArgs += 'vocab1[]=' + vocab1Options[i-1];
    if (
i != vocab1Options.length) getArgs += "&";
  }
 
getPath = getPath + getArgs;
   
 
// make ajax call using get
 
$.get(getPath, args,
    function(
result){
      if (
result) $('#eric_column_right').html(result);
    }
  );
   
}
?>

And, here is some CSS to create a simple 2 column layout:

.clearBoth {
  clear: both;
}

#eric_column_left {
  float: left;
  width: 25%;
}

#eric_column_right {
  float: left;
  width: 75%;
}

After flushing the menu cache, and going to the new page, you should see a 2 column layout: the left column containing a select element with the taxonomy terms and the right column containing the view. When selecting options in the taxonomy select element, they options should be passed to view as arguments, and the view should be reloaded:

The second part of this tutorial is adding the functionality to save the search as a node. There should be a Save button already on the form that does not do anything. We'll need to add a submit handler to save the selected options into a node. So, first you'll need to create a new node type. For my example, I created a node called "saved_search" with a single textarea field called "field_search_parameters". Here's the code I added to make the submit button function:

<?php
// define a submit handler that serializes the selected form data and creates a node
function _ericview_form_submit($form, &$form_state) {

 
// define a list of values to save
  // TODO: make this more dynamic?
 
$saveValues = array('vocab1');
   
 
$savedParams = array();
  foreach (
$form_state['values'] as $k => $v) {
    if (
in_array($k, $saveValues)) {
     
$savedParams[$k] = $v;
    }
  }
   
 
// create saved search node
 
$node = (object) NULL;
 
$node->type = 'saved_search';
 
$nodeData = array();
 
$nodeData['values'] = array(
   
'title' => 'Saved Search: ' . date('r'),
   
'field_search_parameters' => array(
      array(
       
'value' => serialize($savedParams)
      )
    ),
   
'name' => $GLOBALS['user']->name,
   
'op' => t('Save'),
  );
   
 
// include node file, necessary for node generation
 
module_load_include('inc', 'node', 'node.pages');
   
 
// create node using drupal_execute
 
drupal_execute('saved_search_node_form', $nodeData, $node);
   
}
?>

Now when the user clicks on the save button, a node will be created that contains serialized search form data in a field. If you view one of these newly created nodes, you get some unusable serialized data on the screen:

Instead, you can add a nodeapi hook to redirect the user back to the menu callback:

<?php
function ericview_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (
$node->type == 'saved_search' && $op=='view') {
       
   
// unserialize data
   
$data = unserialize($node->field_search_parameters[0]['value']);
       
   
// create query string       
   
$queryString = array();
    foreach (
$data as $k => $v) {
      if (
is_array($v)) {
        foreach (
$v as $v2) {
         
$queryString[] = $k."[]=".$v2;
        }
      } else {
       
$queryString[] = "$k=$v";
      }
    }
   
$queryString = implode("&", $queryString);
       
   
// redirect user
   
drupal_goto('ericview/page', $queryString);
       
  }
}
?>

For my example, I unserialize the data in the node's field and create a query string based on the data. That way, the $_GET parameters will be passed into the form and the view, acting as a saved search.

Eric's picture

In this snippet, I'll explain how to implement an autocompleting field using the forms API and a menu callback. The first thing you'll need to create is the callback function that queries the database and returns an non-themed javascript result. In this function I'll return a list of node IDs and their titles:

<?php
function MYMODULE_autocomplete_node($userString) {
 
// create the SQL to query the node table
 
$sql = "select nid, title from {node} where status='1' and lower(title) like lower('%%%s%%') order by title asc";

 
// query the database
 
$resource = db_query_range($sql, $userString, 0, 10);

 
// loop through the results and create an associative array
 
$results = array();
  while (
$row = db_fetch_array($resource)) $results[$row['nid']] = $row['title'];

 
// output the results in javascript
 
print drupal_to_js($results);

 
// exit, to prevent your results form hitting the theme layer
 
exit();
}
?>

Next, create a menu item...

<?php
function MYMODULE_menu() {
 
$items[] = array();
 
// ...code...
 
$items['MYMODULE/autocomplete/node'] = array(
   
'type' => MENU_CALLBACK,
   
'access arguments' => array('access content'),
   
'page callback' => 'MYMODULE_autocomplete_node',
  );
 
// ...code...
 
return $items;
?>

Now, if you flush your menu cache and go to your newly created menu path, you should see a list of nodeID and their titles formatted in JSON. Lastly, you'll need to create the form element:

<?php
function MYMODULE_form() {
 
$form = array();
 
// ...code...
 
$form['MYFIELDNAME'] = array(
   
'#type' => 'textfield',
   
'#title' => t('MYFIELDTITLE'),
   
'#autocomplete_path' => 'MYMODULE/autocomplete/node',
  );
 
// ...code...
 
return $form;
}
?>

Eric's picture

In this code snippet I'll explain how to create a menu item that has a dynamic title.

<?php
// define hook_menu to create menu item
function MYMODULE_menu() {
 
$items = array();
   
 
$items['MY/PAGE/URL/%'] = array(
   
'page callback' => 'MY_PAGE_CALLBACK_FUNCTION',
   
'title callback' => 'MY_PAGE_CALLBACK_TITLE_FUNCTION',
   
'title arguments' => array(3),
   
'type' => MENU_CALLBACK,
  );

  return
$items;
}

// define title callback function
function MY_PAGE_CALLBACK_TITLE_FUNCTION($arg) {
  return
"My dynamic title: " . $arg;
}
?>

In the above example, if you browsed to the URL: MY/PAGE/URL/blah-blah-blah, you'd have a page title of: My dynamic title blah-blah-blah. In a more applicable example, you might need to pass the node id to the callback function and then return: $node->title;

Check out the documentation on wildcard loader arguments for more advanced options.

Eric's picture

Here's a simple, minimal implementation of AJAX in Drupal using jQuery.

First, create a menu item callback.

<?php
function MYMODULE_menu() {
 
$items = array();
 
$items[] = array(
   
'path' => 'ajax',
   
'title' => t('AJAX'),
   
'type' => MENU_CALLBACK,
   
'callback' => 'MYMODULE_callback_ajax',
   
'access' => true,
  );
  return
$items;
}
?>

Next, create the callback function. To test the menu item and callback function, go to http://[YOURSITE]/ajax; you should get the text "wee!" without any theming.

<?php
function MYMODULE_callback_ajax() {
  echo
'wee!';
  die;
}
?>

Next create a page that has a button on it with an ID of "ajaxButton". The following jQuery will add an onClick event to the button.

<?php
$(document).ready(function(){
  $(
'#ajaxButton').click(function(){
    $.
ajax({
     
type: 'GET',
     
url: '/ajax',
     
success: function(html){
       
alert(html);
      }
    });
  });
});
?>

The jQuery binds a function to the onClick event, with makes the Ajax call to the URL, which simply returns the text "wee!", which is then echoed in a javascript alert box.