Eventos ajax en elementos de página distintos de formulario

14/06/2015
Eventos ajax en elementos de página distintos de formulario

En ocasiones hemos necesitado incluir un evento ajax en elementos de página que no sean formularios para realizar una acción y su inversa. Por ejemplo el tan típico “Me gusta / Ya no me gusta” que podemos conseguir con el módulo Flag.

Pues bien, en este post describiremos la manera de incluir en un enlace de una página, una llamada ajax a un proceso de modificación del valor de un campo de un tipo de contenido o entidad.

Primer paso

En primer lugar, podemos definir un permiso específico para controlar el acceso al enlace y la acción asociada que vamos a definir. Para ello lo implementaremos mediante “hook_permission”.

/**
 * Implements hook_permission().
 */
function mymodule_permission() {
  return array(
    'access ajax link' => array(
      'title' => t('Access ajax link'),
      'description' => t('Access ajax link'),
    ),
  );
}

Segundo paso

A continuación, definiremos dentro del “hook_menu” de nuestro módulo de ejemplo, una nueva entrada con la ruta que “disparará” la acción para el enlace que vamos a incluir.

$items['mymodule/%node/%'] = array(
  'title' => 'Manage field value by ajax link',
  'page callback' => 'mymodule_ajax_link',
  'page arguments' => array(1,2),
  'access arguments' => array('access ajax link'),
  'file' => 'includes/mymodule.pages.inc',
);

En este caso, hemos definido una ruta base más un parámetro mediante el wildcard %, que indicará si estamos realizando una acción o su contraria. (Incluiremos más detalles de este parámetro en el siguiente punto). Dicho parámetro se lo pasaremos a la función de callback y el acceso estará controlado por el permiso que hemos descrito en el paso número 1.

Tercer paso

En este punto vamos a incluir el enlace con el evento ajax dentro de la página deseada. En nuestro caso, por ejemplo, hemos hecho un preprocesado del template de un tipo de contenido para incluir una variable con dicho enlace.

En nuestro ejemplo lo hacemos con la siguiente línea:

//Set link
$vars['mymodule_link'] = mymodule_function_link($vars['node']);

Para la generación del texto del enlace como la ruta del mismo hemos utilizado una función personalizada donde incluir la lógica de funcionamiento.

El contenido de esta función podría ser:

/**
 * Function to create ajax link text and path
 */
function mymodule_function_link($node) {
  //Create wrapper for entity node
  $wrapper = entity_metadata_wrapper('node', $node);
  $nid = $wrapper->nid->raw();

  //Define link container
  $link_wrapper = array(
    '#type' => 'container',
    '#attributes' => array(
      'id' => "link-node-$nid",
      'class' => "ajax-link",
    ),
  );

  //Check if node field has value to change link
  if (!$wrapper->field_custom->raw()) {
    $link_wrapper['content']['link'] = array(
      '#type' => 'link',
      '#title' => t('Do action'),
      '#href' => "mymodule/" . $nid . "/do/nojs",
      '#ajax' => array(
        'wrapper' => "link-node-$nid",
        'method' => 'html',
      ),
    );
  }
  else {
    $link_wrapper['content']['link'] = array(
      '#type' => 'link',
      '#title' => t('Undo action'),
      '#href' => "mymodule/" . $nid . "/undo/nojs",
      '#ajax' => array(
        'wrapper' => "link-node-$nid",
        'method' => 'html',
      ),
    );
  }

  return render($link_wrapper);
}

Hemos creado un wrapper del nodo que estamos pasando como parámetro para facilitarnos el acceso a las propiedades del mismo.

Tras esto hemos definido un contenedor (div) donde incluir el enlace con un identificador css que nos permitirá acceder via DOM al contenido del mismo para poder cambiarlo cuando se realice la acción que se define en el enlace que estamos incluyendo.

A continuación, mediante la comprobación del valor de un campo personalizado, incluimos el enlace con un texto u otro y el path asociado a la acción o su contraria. En este punto nos fijamos en tres propiedades importantes del enlace:

  • #title: Texto del enlace.
  • #href: Ruta del enlace. Incluimos el parametro extra “nojs” para que cuando no esté activado javascript vaya por el proceso normal recargando la página.
  • #ajax -> ‘wrapper’: Identificador del contenedor definido por la propiedad “id”

Cuarto paso

Llegados a este punto tenemos que definir la función de callback de la entrada de menú, que es la que realmente realiza la acción y la que nos permite cambiar el contenido del DOM sin recargar la página.

Un ejemplo de esta función puede ser:

/**
 * Callback for managing ajax link actions.
 */
function mymodule_ajax_link($node, $op, $type = 'ajax') {
  $wrapper = entity_metadata_wrapper('node', $node);

  switch ($op) {
    case 'do':
      $status = 1;
      $message = t('Action done successfully');
      $message_type = 'status';
      break;
    case 'undo':
      $status = 0;
      $message = t('Action undone successfully');
      $message_type = 'status';
      break;
    default:
      $status = FALSE;
      $message_type = 'error';
  }

  //Save changes
  if ($status !== FALSE) {
    $wrapper->field_custom->set($status);
    $wrapper->save();
  }

  // Build the message render array:
  $messaje_output = '<div class="message-$message_type">' . $message . '</div>';
  // Deliver Ajax response
  if ($type == 'ajax') {
    $commands = array();
    $ajax_link = mymodule_function_link($node);

    $commands[] = ajax_command_replace("#link-node-" . $node->nid, $ajax_link);
    $commands[] = ajax_command_append("#link-node-" . $node->nid, $messaje_output);

    $page = array('#type' => 'ajax', '#commands' => $commands);
    ajax_deliver($page);
  }
  else {
    drupal_set_message($message);
    drupal_goto('node/' . $node->nid);
  }
}

Al igual que para el caso anterior, generamos en primer lugar un wrapper de la entidad para facilitar el acceso a las propiedades del nodo (primer parámetro que pasamos a la función).

A continuación, el segundo parámetro que pasamos a la función es la operación que queremos realizar, es decir, la acción o su contraria.

Tras esto, en nuestro ejemplo, cambiamos el valor del campo en función de la operación deseada, y por último, mediante comandos ajax reemplazamos el enlace propiamente dicho por su operación contraria, e incluimos un mensaje con el resultado de la misma.

En el caso que la petición no se realice por ajax, redirigimos al usuario al nodo que estamos actualizando.

Quinto paso

Una vez definida la función de callback, sólo nos queda incluir el enlace que habíamos preprocesado dentro del template del tipo de contenido correspondiente.

<?php if (!empty($mymodule_link)): ?>
<li>
  <?php print $mymodule_link; ?>
</li>
<?php endif; ?>

Y eso es todo, con estos pasos, hemos podido incluir un enlace dentro de la vista de un nodo que permite cambiar via ajax el valor de un campo del mismo. En ese momento hemos cambiado dicho enlace por su operación contraria y hemos incluido un mensaje con el resultado de la acción, todo ello sin recargar la página. Con comandos ajax podemos ampliar la funcionalidad descrita, como por ejemplo cambiar o añadir algún elemento de la página que dependa de la acción realizada.