Posts tagged with ajax

Avatar-eric-london
Created by Eric.London on 2011-06-16
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
In this article, I'll share some code that I use to gray out the screen during AJAX calls to prevent users from clicking on anything, and displays a visual cue to the user that their click request is still being processed.

In the following code, I created a hook_menu() implementation and 2 page callbacks; one to show a clickable link and the other to process the AJAX request.

<?php
/**
 * Implements hook_menu()
 */
function helper_menu() {

  $items = array();

  // define the page callback to show the clickable link  
  $items['shadow-test'] = array(
    'title' => t('Shadow Test'),
    'page callback' => '_helper_page_callback_shadow_test',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  
  // define the page callback to process the AJAX request
  $items['shadow-test-ajax'] = array(
    'title' => t('Shadow Test Ajax'),
    'page callback' => '_helper_page_callback_shadow_test_ajax',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  
  return $items;

}

/**
 * Implements the page callback to show the clickable link
 */
function _helper_page_callback_shadow_test() {

  // include module css
  drupal_add_css(drupal_get_path('module', 'helper') .'/helper.css');

  // include module javascript
  drupal_add_js(drupal_get_path('module', 'helper') .'/helper.js');

  // generate shadow html
  $shadow_html = "<div id='shadow' style='background: url(" . '"' . base_path() . drupal_get_path('module', 'helper') .'/shadow.png' . '"' . ")'></div>";

  // define javascript variables to be passed to the DOM
  $js_vars = array(
    'helper' => array(
      'ajax_path' => base_path() . 'shadow-test-ajax',
      'shadow_html' => $shadow_html,
    ),
  );

  // pass variables to javascript
  drupal_add_js($js_vars, 'setting');

  // create a variable for page output
  $output = "";
  
  // create a clickable link
  // NOT: this link will be overwritten via jQuery
  $output .= l(
    t('Click Me'),
    $_REQUEST['q'],
    array(
      'attributes' => array(
        'class' => 'ajax_clickable',
      ),
    )
  );
  
  // return page output
  return $output;

}

/**
 * Implements page callback to process the AJAX request
 */
function _helper_page_callback_shadow_test_ajax() {

  // wait 2 seconds
  // NOTE: this line is just used to demo the "shadow" effect
  sleep(3);
  
  // return a JSON value
  $ret = new StdClass();
  $ret->status = true;
  print drupal_json($ret);

  die;

}
?>


After flushing my menu cache, and browsing to the first page callback, I see the following:

Shadow Off

I added some CSS to my module's CSS include file (helper.css), to help position the "shadow" full screen:


#shadow {
  z-index: 1000;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none;
}


I created a 50% black transparent PNG image in PhotoShop:

50% black image

I added some jQuery to my module's javascript include file (helper.js), to add the click event to the link, and two functions to hide and show the shadow.



Drupal.behaviors.helper = function(context) {

  // find the link with the matching class, and add a click event
  $('.ajax_clickable').click(function(){
    
    // show shadow
    $(this).show_shadow();

    // make ajax call    
    $.getJSON(
      Drupal.settings.helper.ajax_path,
      function(data) {
        
        // when the ajax call is done, hide the shadow
        $(this).hide_shadow();
        
      }
    );

    // prevent the a tag from actually going anywhere
    return false;
  
  });
  
}

;(function($) {

  // defines the function to show the shadow
  $.fn.show_shadow = function() {
  
    // ensure the shadow does not already exist
    if ($('body #shadow').length == 0) {
      
      // add shadow html, and fade it in
      $('body').append(Drupal.settings.helper.shadow_html).find('#shadow').fadeIn();
          
    }
  
  }

  // defines the function to hide the shadow
  $.fn.hide_shadow = function() {
  
    // fade it out, and then remove it from the dom
    $('#shadow').fadeOut('slow', function() {
      $(this).remove();
    });
  
  }
  
})(jQuery);


Now, when I click on the link the page fades to black and the user cannot click on anything. After the 3 second delay (defined in the AJAX callback), the black shadow fades away.

Shadow On
Avatar-eric-london
Created by Eric.London on 2009-09-13
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
For some time now I've wanted to write a blog entry about using AHAH to create dynamically generated form elements. After a recent conversation at work regarding usability, I now had a real world example to create: how to use tiered taxonomy to dynamically generate a form. This code snippet will show you how to create a form that creates child select dropdowns based on the parent taxonomy term the user selects.

First I established a multi-tier taxonomy called "AHAH":


For this example I created a menu callback to display my initial form:
<?php
function helper_menu() {
  
  $items = array();
  
  $items['ahah-form'] = array(
    'title' => 'AHAH Form',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('_helper_callback_ahah_form'),
    'type' => MENU_CALLBACK,
    'access callback' => 'user_access',
    'access arguments' => array('access content'),
  );
  
  return $items;
  
}
?>


I then defined the page callback to show the initial form:
<?php
function _helper_callback_ahah_form() {

  // define an array to contain form elements
  $form = array();
  
  // define the top level vid
  $vid = 2;
  
  // fetch a tree of taxonomy elements
  $tree = taxonomy_get_tree($vid, 0, -1, 1);
  
  // loop though taxonomy and collect elements
  $options = array();
  foreach ($tree as $key => $value) {
    $options[$value->tid] = $value->name;
  }
  
  // create the first select dropdown input
  $form['select_1'] = array(
    '#type' => 'select',
    '#options' => $options,
    '#title' => t('Select 1'),
    '#size' => 5,
    '#multiple' => false,
    '#ahah' => array(
      'event' => 'change',
      'path' => 'ahah-form-callback',
      'wrapper' => 'wrapper-1',
      'method' => 'replace',
    ),
  );
  
  // pass the top level vid in the form
  $form['ahah_vid'] = array(
    '#type' => 'hidden',
    '#value' => $vid,
  );
  
  // create an empty form element to contain the second taxonomy dropdown
  $form['wrapper_1'] = array(
    '#prefix' => '<div id="wrapper-1">',
    '#suffix' => '</div>',
    '#value' => '&nbsp;',
  );
  
  // add a form submit button 
  $form['submit'] = array(
    '#value' => 'Submit',
    '#type' => 'submit'
  );
  
  return $form;
  
}
?>


The above form callback produces the following:


Next, I defined a callback to handle the AHAH page request:
<?php
// new menu item:
function helper_menu() {
  
  // ...
  
  $items['ahah-form-callback'] = array(
    'title' => 'AHAH Form Callback',
    'page callback' => '_helper_callback_ahah_form_callback',
    'type' => MENU_CALLBACK,
    'access callback' => 'user_access',
    'access arguments' => array('access content'),
  );
  
  // ...

  return $items;
  
}

// and, here's the AHAH callback used to create the new form elements:
function _helper_callback_ahah_form_callback() {
  
  // define a string variable to contain callback output
  $output = "";
  
  // pull the top level vid from the $_POST data
  $vid = $_POST['ahah_vid'];
  
  // pull the selected dropdown from the $_PODT data
  $parentVid = $_POST['select_1'];
  
  // loop through the taxonomy tree and fetch child taxonomies
  $options = array();
  $tree = taxonomy_get_tree($vid, $parentVid, -1, 1);   
  foreach ($tree as $key => $value) {
    $options[$value->tid] = $value->name;
  }
  
  // define the second tier select dropdown element
  $form['select_2'] = array(
    '#type' => 'select',
    '#options' => $options,
    '#title' => t('Select 2'),
    '#size' => 5,
    '#multiple' => false,
  );
  
  // rebuild form object and output new form elements 
  $output .= ahah_render($form, 'select_2');
  
  // render form output as JSON
  print drupal_to_js(array('data' => $output, 'status' => true));
  
  // exit to avoid rendering the theme layer
  exit();
  
}

// Lastly, here's a help function pulled from Nick Lewis's blog to alter the form
// see: http://www.nicklewis.org/node/967
// NOTE: based on poll module, see: poll_choice_js() function in poll.module
function ahah_render($fields, $name) {
  $form_state = array('submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  // Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
  $form = form_get_cache($form_build_id, $form_state);
  $form[$name] = $fields;
  form_set_cache($form_build_id, $form, $form_state);
  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
  );
  // Rebuild the form.
  $form = form_builder($_POST['form_id'], $form, $form_state);

  // Render the new output.
  $new_form = $form[$name];
  return drupal_render($new_form); 
}
?>


The above code allows the user to select an option from the top level tier of taxonomy and the AHAH callback will generate the a select dropdown of the child taxonomies as shown below:



On form submission, you'll see that the options the user selected as stored in $form_state['values']['select_1'] and $form_state['values']['select_2']


Avatar-eric-london
Created by Eric.London on 2009-06-10
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
In this tutorial, I'll show you how you can expose your search form on another site using jQuery. At first, I thought about scraping the form's html using AJAX.. and quickly remembered you cannot easily do that. Which lead me to review the AJAX functionality included in jQuery. Bingo, one of my favorites: jQuery.getJSON. To summarize this code, I create a callback function to display the form's json-ified html which can then be easily embedded on another site.

First I defined the menu hook:
<?php
function MYMODULE_menu() {

  $items = array();

  // add a page callback for the url: "external-search.js"
  $items['external-search.js'] = array(
    'page callback' => '_MYMODULE_external_search',
    'type' => MENU_CALLBACK,
    'access arguments' => array('search content'),
  );
    
  return $items;
    
}
?>


Then I created the callback function for the menu callback:
<?php
function _MYMODULE_external_search() {

  // create a json string of the search form html
  $json = drupal_to_js(drupal_get_form('search_form'));
    
  // format the json as a callback function
  // see: http://docs.jquery.com/Ajax/jQuery.getJSON for more information
  if ($_GET['jsoncallback']) {
    $json = $_GET['jsoncallback'] . "(" . $json . ");";
  }
    
  // output the json
  print $json;

  // stop the script, so the theme layer is not applied
  die;
}
?>


One problem though, the form submits locally. That can be fixed using a form_alter function:
<?php
function MYMODULE_form_alter(&$form, $form_state, $form_id) {
    
  // check for external search form and set form action to be full path
  if ($form_id == 'search_form' && arg(0)=='external-search.js') {
    // change the form action to be the full path
    $form['#action'] = 'http://' . $_SERVER['HTTP_HOST'] . $form['#action']; 
  }
}
?>


Now, if you clear your cache and go to http://YOURSITE/external-search.js, you should see the JSON (and nothing else).

Lastly, you can embed the code on another site using a few lines of jQuery. You can even pull the jQuery from your site if the external site does not have jQuery included.



<!-- Include jQuery (as necessary) -->
<script type='text/javascript' src='http://YOURSITE/misc/jquery.js' ></script>

<!-- create a div container to contain the search form -->
<div id='embedded_search'></div>

<!-- add the jQuery to embed the form -->
<script type='text/javascript'>
$(document).ready(function(){
  // make the ajax request
  $.getJSON("http://YOURSITE/external-search.js?jsoncallback=?",
    function(data){
      // append the form to the container
      $('#embedded_search').append(data);            
    }
  );
});
</script>



Now people should be able to access your site's search form from another site!
Avatar-eric-london
Created by Eric.London on 2009-01-23
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
I recently was tasked with tracking when a user submitted a form, but in this case, the form was submitted to an external site. Normally, if the form was being submitted locally I could have added a form_alter hook function and modify the submit handlers. The following code snippet will explain how you can add a javascript submit event on the form, and using a synchronous AJAX call, execute server side code before the user leaves the site.

First, I setup a page callback used by the AJAX call to execute my inserted code.

<?php
// define the menu callback item
function MYMODULE_menu() {
  $items = array();
    
  $items[] = array(
    'path' => 'MYAJAXPATH',
    'title' => NULL,
    'callback' => '_MYAJAXPATH_callback',
    'access' => true,
    'type' => MENU_CALLBACK,
  );
    
  return $items;
}

// define the menu callback function
function _MYAJAXPATH_callback() {
  // here, you could add any code you'd like (modify session data, execute some SQL, etc).
    
  // I added the die statement to prevent the theme layer from being executed
  die;
}
?>


Make sure the page callback works by browsing to it. If you don't output any HTML in your callback function, you should just get a blank page.

Next, you'll need to attach a submit handler to the form and make the synchronous AJAX call.

<?php
$(document).ready(function(){
  $('form#FORM-ID-THAT-SUBMITS-EXTERNAL').submit(function(){
    $.ajax({
      type: "GET",
      url: "/MYAJAXPATH",
      cache: false,
      async: false,
    });
  });
});
?>


I added a few properties to the AJAX request object to enable this functionality: cache set to false ensures the page request will not be pulled from your browser cache & async set to false will ensure the form is not submitted until the AJAX request is done. jQuery's default AJAX calls are set to asynchronous, which does not guarantee the request will be made before the form is submitted.
Avatar-eric-london
Created by Eric.London on 2008-11-20
Tags:
New Comment
 
Please note: the content on this page orginates from ericlondon.com.
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.