Drupal 7 Using ctools' modal frames and field collection forms to create a better user experience

Posted by: 
Dominique De Cooman

We want to use ctools' modal frames and field collection forms to create a better user experience.

As we know, ctools comes with a lot of useful apis and tools to use in our own modules. One of them is the modal frames.

The field collection module which is a lesser known module but non the less very useful allows you to have multiple collections of fields to be added to an entity. For example in our example we have a curriculum vitae and we want to add multiple experiences to it. An experience consist out of several fields: company, from to date, function and description. So the field collection groups them.

To add new expierences we can edit the node or we can use the field collection modules links which go to a seperate page to fill in a seperate form.

What we want to do is make this form appear in a ctools modal frame so we can have easy in place editing which provides a superior user experience.

The example can be used to display any form in a ctools modal frame.

How to?
Install ctools and field collection
Create a node type curriculum and add all the fields. Use the field collection module to add new collections here admin/structure/field-collections. You ll notice it is also an entity so add fields like you add fields to other entities like nodes. Now you can add the collections you made as a field onto your node type cv. Now you should be able to see the links when creating a node.

Next we are going to take over these links. We ll create a hook menu in our custom module to create our proper links. We just prefix and suffix the existing paths because we still need the parameters

<?php
/**
 * Implements hook_menu().
 */
function glue_menu() {
  
$items = array();
   
  
$items['modal/field-collection/%/%/%/%/%ctools_js/go'] = array(
    
'page callback' => 'glue_modal_operator_callback',
    
'page arguments' => array(2,3,4,5,6),
    
'access arguments' => array('access content'),
  );
  
  return 
$items;
}
?>

Now lets create the main function which will take the arguments and create the proper add, edit or delete form to administer our field collections items in a ctools modal frame. (read the comments in code for more explaination)

<?php
/**
 *  Modal callback
 */
function glue_modal_operator_callback($field_name$nid$operator$id 0$js FALSE) {
  
//We need a function to load an argument to use in our form state for a file in the field collection which is not loaded in this context
  
module_load_include('pages.inc''field_collection');  
  
  
//Access checks to make sure the user has access to the field collections
  
switch ($opertor) {
    case 
'add':
      
$result field_collection_item_add($field_name'node'$nid);
      if (
$result == MENU_NOT_FOUND || $result == MENU_ACCESS_DENIED) {
        exit();
      }     
      break;
    case 
'edit':           
    case 
'delete':
      if (!
field_collection_item_access($opertor$id)) {
        exit();
      }
      break;
  }  
  
  
//Check if js is enabled, this parameter will be loaded by ctools
  
if ($js) {
    
//Include ctools ajax and modal, dont forget to set ajax TRUE
    
ctools_include('ajax');
    
ctools_include('modal');
    
$form_state = array(
      
'ajax' => TRUE,
      
'title' => t('Experiences'),
    );
    
    if (
$operator == 'add') {
      
//Arguments need to be loaded directly onto the build_info args array because ctools_modal_form_wrapper will call drupal_build_form() directly see from API for more
      
$arg glue_field_collection_item_add(str_replace('-''_'$field_name), 'node'$nid);
      if (
$arg == MENU_NOT_FOUND || $arg == MENU_ACCESS_DENIED) {
        exit();
      }
      
$form_state['build_info']['args'][] = $arg
      
//The modal form wrapper is needed to make sure the form will allow validating, you cannot use drupal_get_form directly it wont work.
      
$output ctools_modal_form_wrapper('field_collection_item_form'$form_state);
    }
    else {
      
//The id is the collection entity id
      
$form_state['build_info']['args'][] = field_collection_item_load($id);
      if (
$operator == 'edit') {
        
$output ctools_modal_form_wrapper('field_collection_item_form'$form_state);
      }
      elseif (
$operator == 'delete') {
        
$output ctools_modal_form_wrapper('field_collection_item_delete_confirm'$form_state);
      }
      else {
        exit();
      }
    }
        
    
//If the form is executed will need to dismiss the form and reload the page
    
if ($form_state['executed']) {      
      
$commands = array();
      
      
//Load the new output
      
$node node_load($nidNULLfalse);       
      
//Render the newly saved field collection set       
      //Here is how to render a single field:<a href="http://dominiquedecooman.com/blog/drupal-7-tip-theming-render-only-single-field-your-entities
">http://dominiquedecooman.com/blog/drupal-7-tip-theming-render-only-singl...</a>      $field_to_render = field_view_field('node', $node, str_replace('-', '_', $field_name), 'full');     

      // Remove the prefix and suffix, which contain unneeded div's and actions links.
      unset(
$field_to_render['#prefix']);
      unset(
$field_to_render['#suffix']);

      
$output = render($field_to_render);
      
      //We will replace the fieldcollection with the new output
      
$commands[] = ajax_command_html('.field-name-field-cv-experience', $output);
      //close the frame
      
$commands[] = ctools_modal_command_dismiss();
      
      
$output = $commands;
    }
    //Render the output
    print ajax_render(
$output);
    exit();        
  }
  else {
    //No js found lets go to the default page
    return drupal_get_form('field_collection_item_form', field_collection_item_load(
$id));
  }
}

/**
 * Add a new field-collection item.
 * 
 * We copied this function from the field collection module but instead of returning a form we return the object
 */
function glue_field_collection_item_add(
$field_name$entity_type$entity_id$revision_id = NULL, $langcode = NULL) {
  
$info = entity_get_info();
  if (!isset(
$info[$entity_type])) {
    return MENU_NOT_FOUND;
  }
  
$result = entity_load($entity_type, array($entity_id));

  
$entity = reset($result);  

  if (!
$entity) {
    return MENU_NOT_FOUND;
  }
  // Ensure the given entity is of a bundle that has an instance of the field.
  list(
$id$rev_id$bundle) = entity_extract_ids($entity_type$entity);
  
$instance = field_info_instance($entity_type$field_name$bundle);

  if (!
$instance) {
    return MENU_NOT_FOUND;
  }

  // Check field cardinality.
  
$field = field_info_field($field_name);
  
$langcode = LANGUAGE_NONE;
  if (!(
$field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || !isset($entity->{$field_name}[$langcode]) || count($entity->{$field_name}[$langcode]) < $field['cardinality'])) {
    drupal_set_message(t('Too many items.'), 'error');
    return '';
  }

  
$title = ($field['cardinality'] == 1) ? $instance['label'] : t('Add new !instance_label', array('!instance_label' => drupal_strtolower($instance['label'])));

  drupal_set_title(
$title);

  
$field_collection_item = entity_create('field_collection_item', array('field_name' => $field_name));
  // Do not link the field-collection item with the host entity at this point,
  // as during the form-workflow we have multiple field-collection item entity
  // instances, which we don't want link all with the host.
  // That way the link is going to be created when the item is saved.
  
$field_collection_item->setHostEntity($entity_type$entity, LANGUAGE_NONE, FALSE);

  // Make sure the current user has access to create a field collection item.
  if (!field_collection_item_access('create', 
$field_collection_item)) {
    return MENU_ACCESS_DENIED;
  }
  return 
$field_collection_item;
}
?>

The only thing we need to do now is take over the links on the page. For this to happen we will alter the render array. The hook to do this is hook_field_attach_view_alter().

It will be by adding the ctools-use-modal class that our modal functionality will be triggered.

<?php
/**
 * Implements hook_field_attach_view_alter
 */
function synergie_field_attach_view_alter(&$output$context) {
  if (
$output['field_cv_experience']) {
    
ctools_include('modal');
    
ctools_modal_add_js();
    
    
$field 'field_cv_experience';
    
//Add
     
$output[$field]['#suffix'] = 
    
'<div class="description field-collection-description"></div>
       <ul class="action-links action-links-field-collection-add">
         <li>'
           
l(t('Add'), 'modal/field-collection/field-cv-experience/' $context['entity']->nid '/add/0/nojs/go'
               array(
'attributes' => array('class' => 'ctools-use-modal'))) .
         
'</li>
        </ul>
     </div>'
;
    
    
//Edit & delete        
    
foreach ($output[$field] as $key => $value) {      
      if (
is_numeric($key)) {          

        
$output[$field][$key]['links']['#links']['edit']['href'] = 'modal/field-collection/' str_replace('_''-'$field) . '/' $context['entity']->nid   '/edit/' $output[$field]['#items'][$key]['value'] . '/nojs/go';
        
$output[$field][$key]['links']['#links']['edit']['attributes'] = array('class' => 'ctools-use-modal');

        
$output[$field][$key]['links']['#links']['delete']['href'] = 'modal/field-collection/' str_replace('_''-'$field) . '/' $context['entity']->nid '/delete/' $output[$field]['#items'][$key]['value'] . '/nojs/go';
        
$output[$field][$key]['links']['#links']['delete']['attributes'] = array('class' => 'ctools-use-modal');
      }      
    }
  }
}
?>

Here is how to do it and this is what the result should look like:

You should be able to allow validating and after submission thanks to the ctools_modal_form_wrapper function. After submission the modal frame should close and the page should show the changes that where inserted by the ajax_command_html().

For more on how to theme the modal frame and to do much with them more see ctools_ajax_sample.module which comes with ctools.

Add new comment

Who are you?


By knowing who you are you will be directed to a page where we have displayed all our drupal services relevant to your professional identity.