Archivo de la etiqueta: FAPI

druplicon

Drupal 7: Campos dependientes en formularios con Ctools

Hola gente como están. Hoy voy a hablar de campos dependientes en formularios.

Ctools entre las utilidades que nos da, nos proporciona la posibilidad de indicar cuales elementos de formulario dependen de otros.
Puede parecer algo que añada complejidad, pero nada mas lejos de la realidad. De hecho, para implementarlo solo necesitas dos lineas de código.

Elementos

Los elementos que son capaces de mostrar contenidos en base a lo seleccionado son:

  • checkbox/checkboxes
  • radios
  • select

Luego está lo que se muestre

Cualquier elemento FAPI es susceptible de ser dependiente de alguno de los elementos mencionados antes. por ejemplo, un textfield, un textarea, otros select, e incluso fieldsets.

Acá van tres ejemplos:

select

Es el mas simple de todos:

<?php
function formulario_ejemplo_de_selects() {

  ctools_include('dependent');

  $form['pais_select'] = array(
    '#title' => 'Selecciona un país',
    '#type' => 'select',
    '#required' => TRUE,
    '#options' => array(
      "es" => "España",
      "other" => "Otro"
    ),
  );

  $form['provincias_espanolas'] = array(
    '#title' => 'Selecciona tu provincia',
    '#type' => 'select',
    '#options' => array(
      0 => "Alava",
      1 => "Albacete",
      2 => "Etcétera"
    ),
    '#dependency' => array('edit-pais-select' => array('es')),
  );

  $form['provincia_extrangera'] = array(
    '#title' => t('Provincia'),
    '#description' => t('Indica la provincia de tu país'),
    '#type' => 'textfield',
    '#required' => TRUE,
    '#dependency' => array('edit-pais-select' => array('other')),
  );

  return $form;
}

Se aprecia rápidamente que es un formulario normal de toda la vida pero que tiene dos extras:

ctools_include(‘dependent’); Necesario en todo formulario que quiera hacer uso de elementos dependientes ya que es el plugin que nos lo permite.
‘#dependency’ => array(‘DOM id sin “#”‘ => array(‘un_valor_del_select’)), Es la propiedad que le dice a este elemento de formulario de quien depende. para que funcione solo tenés que indicarle cual es el DOM id del select (en este caso es #edit-pais-select) y cual es el valor que espera para mostrarse.

Explicado lo anterior se entiende bastante bien que se va a mostrar el select de provincias españolas solo si la opción seleccionada en $form['pais_select'] es “es” por ejemplo.

Y esa es la base del plugin. los radio y checkbox tienen unas particularidades así que dejo los ejemplos respectivos para aclararlo.

radios

Para usar radios es necesario cambiar un poco el selector que usamos en #dependency.

<?php '#dependency' => array('radio:pais_radios' => array('other')),

Como pueden ver y a no estamos usando el DOM id, sino que en su lugar estamos usando “radio:” seguido del nombre del field “pais_radios“. Si se preguntan por que es así en los radios es porque necesita hacer referencia a la propiedad “name” del elemento ya renderizado (pueden inspeccionar los name de los radios en firebug para confirmarlo).

Aquí el ejemplo:

<?php
function formulario_ejemplo_de_radios() {

  ctools_include('dependent');

  $form['pais_radios'] = array(
    '#title' => 'Selecciona un país',
    '#type' => 'radios',
    '#required' => TRUE,
    '#options' => array(
      "es" => "España",
      "other" => "Otro"
    ),
    '#default_value' => "other"
  );

  $form['provincias_espanolas'] = array(
    '#title' => 'Selecciona tu provincia',
    '#type' => 'select',
    '#options' => array(
      0 => "Alava",
      1 => "Albacete",
      2 => "Etcétera"
    ),
    '#dependency' => array('radio:pais_radios' => array('es')),
  );

  $form['provincia_extrangera'] = array(
    '#title' => t('Provincia'),
    '#description' => t('Indica la provincia de tu país'),
    '#type' => 'textfield',
    '#required' => TRUE,
    '#dependency' => array('radio:pais_radios' => array('other')),
  );

  return $form;
}

Checkbox/es

Nuevamente en los checkboxes #dependency cambia ligeramente. Seguimos usando el DOM id pero cada uno de los checkboxes tiene su propio id y el valor no es ni “es” ni “other” (son los ejemplos que estábamos usando en los formularios de muestra). En su lugar hay que usar 1 o 0 para indicar si queremos que los elementos se muestren cuando esté activo o inactivo.
Como soy malisimo explicando mejor les dejo el ejemplo:

<?php
function formulario_ejemplo_de_checkboxes() {

  ctools_include('dependent');

  $form['pais_select'] = array(
    '#title' => 'Selecciona un país',
    '#type' => 'checkboxes',
    '#required' => TRUE,
    '#options' => array(
      "es" => "España",
      "other" => "Otro"
    ),
  );

  $form['provincias_espanolas'] = array(
    '#title' => 'Selecciona tu provincia',
    '#type' => 'select',
    '#options' => array(
      0 => "Alava",
      1 => "Albacete",
      2 => "Etcétera"
    ),
    '#dependency' => array('edit-pais-select-es' => array(1)),
  );

  $form['provincia_extrangera'] = array(
    '#title' => t('Provincia'),
    '#description' => t('Indica la provincia de tu país'),
    '#type' => 'textfield',
    '#required' => TRUE,
    '#dependency' => array('edit-pais-select-other' => array(1)),
  );

  return $form;
}

Para que puedan probarlo les adjunto un modulo con los ejemplos. Para probar cada uno de los ejemplos accedan a las siguientes URL:

  • /formulario-dependiente-de-select
  • /formulario-dependiente-de-radios
  • /formulario-dependiente-de-checkboxes

Descargar modulo de ejemplo

Chau!

druplicon

Como hacer formularios multistep (wizard) con Ctools

Chaos tools para muchos no es mas que una dependencia de Panels, Views, Features y muchos otros y fabulosos módulos mas. Pero (Gran pero), Ctools per se es un modulo increíblemente útil para nosotros mismos.

Bueno vamos a lo que vamos. Acá les dejo la idea de lo que vamos a hacer:

Tengo un formulario que necesita recoger tres datos de un usuario, y me parece que es mucha información para pedirla toda junta así que vamos a hacerlo en tres pasos distintos.

Para hacer este wizard vamos a usar un formulario normal pero separado en 3 funciones, y dos de las herramientas (plugins) que tiene Ctools. La primera es obviamente “Wizard” y para la persistencia de los datos entre steps usaremos “object-cache“.

Antes la estructura:

  • Necesitamos hacer un item de menú con hook_menu() que nos lleve a una función que es la que va a manejar la batuta.
  • 3 mini funciones para manejar “object-cache
  • 3 formularios/funciones para declarar el contenido de cada uno de los step.

Hagamos lo:

Primero: el menú y el callback del menú:

<?php
function ctools_multistep_menu() {

  $items['ctools_multistep/example_multistep_form'] = array(
    'title' => 'Ejemplo de un wizard con ctools',
    'page callback' => 'wizard_callback',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );

  return $items;
}

function wizard_callback($step = NULL) {
 $form_info = array(
    'id' => 'example_multistep_form',

    'path' => "ctools_multistep/example_multistep_form/%step",
    'return path' => "",

    'show trail' => TRUE,
    'show back' => TRUE,

    'next callback' => 'callback_paso_siguiente',
    'finish callback' => 'callback_paso_final',

    'forms' => array(
      'primer_formulario' => array(
        'form id' => 'primer_formulario',
        'title' => 'Primer paso',
        'include' => drupal_get_path("module", "ctools_multistep") . '/formularios.inc',
      ),
      'segundo_formulario' => array(
        'form id' => 'segundo_formulario',
        'title' => 'Segundo paso',
        'include' => drupal_get_path("module", "ctools_multistep") . '/formularios.inc',
      ),
      'tercer_formulario' => array(
        'form id' => 'tercer_formulario',
        'title' => 'Tercer paso',
        'include' => drupal_get_path("module", "ctools_multistep") . '/formularios.inc',
      ),
    ),
  );

  global $user;
  $object_id = "ctools_multistep_de_ejemplo_" . $user->uid;

  if (empty($step)) {
    clean_datos($object_id);
    $step = 'primer_formulario';
  }

  $object = get_datos($object_id);

  $form_state = array(
    'object_id' => $object_id,
    'object' => &$object,
  );

  ctools_include('wizard');
  $form = ctools_wizard_multistep_form($form_info, $step, $form_state);
  return drupal_render($form);
}

Como pueden ver, el item de menú no es mas que una URL normal.

Ahora bien. En la función principal podemos ver como definimos inicialmente un array normalito, y es en el en donde definimos las características del wizard:

‘id’ => ‘example_multistep_form’
id (string) es el nombre que va a tener el wizard y es obligatorio poner uno

‘path’ => “ctools_multistep/example_multistep_form/%step”,
path (url) es la URL que definimos en el hook_menu() mas “%step”. y es la URL que va a usar Ctools para ir poniendo el step actual en la arra de direcciones de nuestro navegador.

‘return path’ => “<front>”
return path (url) es la URL a la que queremos enviar al usuario cuando este haya completado el formulario. en mi caso lo quiero mandar a la home

‘show trail’ => TRUE
show trail (bool) indica a wizard que queremos que se muestre el breadcrumb de cada uno de los pasos.

‘show back’ => TRUE
show back (bool) indica si queremos dejar que un usuario vuelva a pantallas anteriores. si está en TRUE muestra el botón con dicho fin.

‘next callback’ => ‘callback_paso_siguiente’
next callback (function name) indica cual es la función que se debe ejecutar cada vez que pasemos de un formulario al siguiente. y es en esta funcion en la que vamos a ir guardando el estado actual del usuario.

‘finish callback’ => ‘callback_paso_final’
finish callback (function name) indica cual es la función que se debe ejecutar una vez que el usuario haya completado el formulario. Aquí es donde vamos a guardar de forma permanente los datos del formulario y a limpiar los datos temporales.

‘forms’ => array(
‘primer_formulario’ => array(
‘form id’ => ‘primer_formulario’,
‘title’ => ‘Primer paso’,
‘include’ => drupal_get_path(“module”, “ctools_multistep”) . ‘/formularios.inc’,
),
‘segundo_formulario’ => array(
‘form id’ => ‘segundo_formulario’,
‘title’ => ‘Segundo paso’,
‘include’ => drupal_get_path(“module”, “ctools_multistep”) . ‘/formularios.inc’,
),
)

Por ultimo en forms (array) indicaremos cuales son los formularios que se van a usar en cada paso, el titulo que van a mostrar en el breadcrumb y si tuviéramos esta función en otro archivo, la ruta en la que se aloja.

Nos quedan dos cosas en el formulario. La primera es definir el nombre del objeto que va a almacenar de forma temporal los datos del usuario, ver si ver si vamos a reiniciar el formulario (en el caso de que el usuario acceda nuevamente al formulario) e inicializar los datos del usuario (ahora lo explico).

<?php
global $user;
$object_id = "ctools_multistep_de_ejemplo_" . $user->uid;

if (empty($step)) {
  clean_datos($object_id);
  $step = 'primer_formulario';
}

  $object = get_datos($object_id);

La segunda cosa que nos queda en esta función es claramente generar el wizard:

<?php
$form_state = array( 'object_id' => $object_id, 'object' => &$object );

ctools_include('wizard');
$form = ctools_wizard_multistep_form($form_info, $step, $form_state);
return drupal_render($form);

mmmbueeeeno, tenemos declarado el formulario, un par de funciones para el avance y finalización y hacemos uso de clean_datos() y get_datos(). pues eso es lo que sigue. definir estas funciones:

set_datos(), get_datos() y clean_datos() no son mas que unos wrappers que guardan recuperan y eliminan la información que el usuario va rellenando:

<?php
function set_datos($id, $object) {
  ctools_include('object-cache');
  ctools_object_cache_set('ctools_multistep', $id, $object);
}

function get_datos($id) {
  ctools_include('object-cache');
  $object = ctools_object_cache_get('ctools_multistep', $id);

  //creamos un objeto vacío para empezar
  if (!$object) {
    $object = new stdClass;
    $object->dia = NULL;
    $object->nombre = "";
    $object->apellido = "";
  }

  return $object;
}

function clean_datos($id) {
  ctools_include('object-cache');
  ctools_object_cache_clear('ctools_multistep', $id);
}

Con las pequeñas funciones de object-cache vamos metiendo la información que el usuario complete cada vez que se le de a “Continuar” dentro del objeto:

<?php
function callback_paso_siguiente(&$form_state) {
  set_datos($form_state['object_id'], $form_state['object']);
}

Y cuando el usuario complete el formulario vamos a usar la función que definimos en “finish callback” para dejarle un mensaje en pantalla al usuario (lo mas lógico seria guardarlos de forma permanente pero esta es una demo XD) y limpiar los datos;

<?php
function callback_paso_final(&$form_state) {
  $obj = $form_state["object"];
  drupal_set_message($obj->nombre . " " . $obj->apellido . " gracias por seleccionar el día " . $obj->dia);
  clean_datos($form_state["object_id"]);
}

Y eso es básicamente todo lo que necesitamos. Faltan como es lógico, las funciones que contienen los formularios junto a su respectivo _validate() y _submit(). Les pongo uno de ejemplo para que vean que no tiene nada del otro mundo:

<?php
function segundo_formulario($form, &$form_state) {
  $form['nombre'] = array(
    '#type' => 'textfield',
    '#title' => t('Dime tu nombre'),
    '#default_value' => $form_state['object']->nombre,
    '#required' => TRUE,
  );

  return $form;
}

function segundo_formulario_validate($form, &$form_state) {
}

function segundo_formulario_submit($form, &$form_state) {
  $form_state['object']->nombre = $form_state['values']['nombre'];
}

Aunque ahora que me doy cuenta, quiero comentar que en el _submit() (que siempre se ejecuta antes que la función datos callback_paso_siguiente() que almacena los datos), es el punto en el que seteamos el valor que el usuario acaba de rellenar en la correspondiente propiedad del objeto que esta por ser almacenado.

Todo lo anterior nos va a generar un formulario como el siguiente:

y cuando el wizard nos mande a la home vamos a ver:

Les dejo un descargable con este wizard de ejemplo para que puedan verlo en detalle.

Descargar

Chau!

druplicon

Crear múltiples instancias de un mismo formulario en Drupal 6

El problema mas común a la hora de querer hacer que un formulario se repita mas de una vez en una misma pagina, es que cada uno de los formularios que se vayan creando van a ser solamente una fiel copia del anterior. Dicho de otra forma, estamos usando un solo formulario pero copiandolo en varios lados de la pagina.

Tuve este problema hace ya mas de un año pero no me olvido lo JODIDA-MENTE DIFÍCIL que me fue encontrar una solución, hasta que me di con esta pagina. Cuestión, que me salvó la vida (exagerado yo?). Por supuesto que les recomiendo que lean el articulo original, pero mas les recomiendo que lean mi interpretación de los hechos que esta mas buena :) .

Para este ejemplo primero vamos a crear una url donde poder poner nuestras múltiples instancias:

<?php
/**
 * Implementation of hook_menu()
 *
 * Solo esta implementada una URL para poder hacer este ejemplo.
 */
function form_multi_instancias_menu() {
    $items['form-multiples-instancias'] = array(
        'title' => t('Formulario multi instanciado'),
        'page callback' => 'form_multiples_instancias',
        'type' => MENU_NORMAL_ITEM,
        'access callback' => TRUE,
    );
    return $items;
}

Y la respectiva función:

<?php
/**
 * En este callback de la url queremos crear varias instancias
 * del mismo formulario.
 */
function form_multiples_instancias() {
    $salida = 'Si le hechas una mirada al codigo fuente vas a poder ver que el "form_id" de cada formulario es distinto, asi como el "form_build_id".';

    //en este ejemplo simplemente usamos un contador para crear los
    //formularios, pero podriamos estar procesando nodos tranquilamente.
    for ($i = 0; $i < 5; $i++) {
        /*
          * Noten que estoy armando dinamicamente el
          * drupal_get_form(). Lo siguiente equivaldria a hacer:
          * $salida .= drupal_get_form ( "form_ejemplo_0" , 0);
          * $salida .= drupal_get_form ( "form_ejemplo_1" , 1);
          * $salida .= drupal_get_form ( "form_ejemplo_2" , 2);
          * etc...
          */
        $salida .= drupal_get_form("form_ejemplo_" . $i, $i);
    }

    return $salida;
}

Si van leyendo los comentarios del código se va a ir entendiendo. Definamos el formulario que queremos instanciar:

<?php
//Este es un FAPI comun de toda la vida
function form_ejemplo($form_state, $i) {
    $form['un_campo_de_texto'] = array(
        '#type' => 'textfield',
        '#title' => t('Un campo de texto de ejemplo'),
        '#default_value' => $i,
    );
    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => "enviame",
    );

    $form['#validate'] = array("form_ejemplo_validate");
    $form['#submit'] = array("form_ejemplo_submit");
    return $form;
}

function form_ejemplo_validate($form_state, $form) {
    drupal_set_message("He validado.");
}

function form_ejemplo_submit($form_state, $form) {
    drupal_set_message(
        t('Y el formulario enviado fue el Nº @form_number.',
            array(
                '@form_number' => $form_state ["un_campo_de_texto"] ["#value"])
        )
    );
}

Bueno bueno bueno… hasta acá, si ejecutas el script lo que pasaría es que te saldrían tantos warning como formularios se invoquen. Y eso es debido a que no tenemos ningún formulario definido que se llame form_ejemplo_0, ni form_ejemplo_1, etc.

¿Soluciones?: Crear de la función form_ejemplo_0() para delante jajaja. No enserio, la solución es implementar el hook_forms(), que básicamente va a entender que se esta invocando un formulario inexistente y va a redireccionarlo a la definición ÚNICA que tenemos: form_ejemplo().

<?php
/**
 * Acá es donde se va a hacer la magia:
 * Resulta que por cada formulario que se invoca, el
 * hook_forms() es capaz de actuar como si de un hook_form_alter()
 * se tratara pero lo hace antes que se termine de armar
 * el array FAPI mas basico que crea Drupal.
 * O sea que intercede antes de que se le genere a cada
 * formulario sus #form_build_id, lo que nos permite
 * sobreescribir ciertos aspectos como ser la funcion
 * que alimentará el futuro formulario.
 */
function form_multi_instancias_forms($form_id, $args) {
    $forms = array();
    //para no cambiarle el callback a todos los formularios
    //que se creen hacemos un pequeño if. caso contrario
    //tendriamos un problema GORDO.
    if (strpos($form_id, 'form_ejemplo_') === 0) {
        //hemos dado con una de las variantes del formulario que
        //estamos instanciando, solamente tenemos que apuntar
        //su callback al formulario que hemos definido.
        $forms[$form_id] = array('callback' => 'form_ejemplo');
    }

    return $forms;
}

Bueno. ya está. este código así, funciona. Les dejo un mini-modulo con lo anterior para que lo vean funcionar. Instalenlo y miren en la URL “/form-multiples-instancias

DESCARGAR