La Página de DriverOp

Listas dependientes.

Por Diego Romero.

En este artículo resolveré el problema de hacer que dos listas tipo select (o listbox) sea dependiente el segundo respecto del primero, es decir que el segundo select cambie de valores dependiendo del valor seleccionado en el primero. Este artículo tiene una segunda parte.

Índice

1. Planteamiento del problema.

Para resolver este problema haré uso de PHP y JavaScript. La idea es que el usuario tenga dos elementos HTML tipo select o lista descolgable, el primero con una lista de valores y el segundo se cargará automáticamente con otros valores dependiendo del valor seleccionado por el usuario en el primero. Esto es lo que se llama en base de datos una relación "1 a muchos".

Primero necesitamos una fuente de datos, que puede ser el resultado de una consulta a una base de datos o cualquier otra fuente de datos que el programador disponga. Para propósitos de este artículo y a modo de ejemplo, usaré un archivo de texto el cual contiene todos los valores posibles que se cargarán en el segundo select. Los ejemplos mostrados aquí los presento de la forma más sencilla posible despreciando todo lo que tiene que ver con diseño HTML concentrándome únicamente en obtener la funcionalidad requerida, otros detalles se los dejo al criterio del lector. Usaré como ejemplo un caso típico: se trata de tener en el primer select una lista de países (tres en mi caso) y en el segundo una lista de províncias o estados que pertenecen a esos países, al seleccionar un país en el primer select se cargarán en el segundo solamente aquellos estados o províncias que pertenezcan al país seleccionado.

La página HTML.

Entonces, el estado inicial de la página HTML que contiene el formulario en cuestión sería así:

Formulario básico


Ejemplo de select dependientes
  


  

Aquí tenemos los dos select más un botón "Enviar" dentro de un formulario web, el primer select tiene la lista de países. Por sí mismo este formulario web no realiza la funcionalidad propuesta, me servirá como base para continuar el desarrollo de la solución paso por paso.

La ayuda de JavaScript.

El problema requiere que sea capaz de determinar qué valor ha seleccionado el usuario en el primer select para rellenar el segundo con los valores apropiados. Por lo tanto el siguiente problema a resolver es cómo determinar que el usuario ha seleccionado un país de la lista que está en el select "selector1". Para ello voy a recurrir al evento onChange el cual al ser disparado llamará a una función JavaScript.

Esta función debe determinar que el valor seleccionado es un valor válido y no el valor que le indica al usuario que seleccione un país, como se ve en el código HTML el primer valor que está por omisión no es nada más que un mensaje al usuario dándole la pista de lo que debe hacer, a esa entrada de la lista le he puesto un valor "null" que me ayudará a determinar si el usuario efectivamente ha seleccionado un país o no.

Entonces, en el primer select voy a asignar una función al evento onChange:

Evento onChange

    
    
    
    
  
  
  



Como adelanté al principio de este artículo el lector tendrá que implementar la recuperación de datos que crea conveniente en los lugares indicados en el código, en mi caso, como también mencioné ya, yo lo haré leyendo de un archivo de texto, lo que a continuación implemento será solamente a título demostrativo.

Rellenando el segundo select.

El archivo de texto en cuestión tiene un formato que "me inventé" para hacer la recuperación de datos más cómoda, haré fuerte uso de la función PHP explode(); la cual me permite dividir una cadena de texto usando un caracter especial como "token" y devuelve el resultado en la forma de un array. El formato del archivo tiene la siguiente sintaxis:

Formato del archivo de texto
Cod_ISO_país=Cod_Provincia:Nombre*Cod_Provincia:Nombre*Cod_Provincia:Nombre

Cada línea del archivo se corresponde con un país. Para leer extrayendo los datos de este archivo escribiré una función que tomará como parámetro el código ISO del país (y que está como valor en el select "selector1" y devolverá la línea de texto que le corresponde menos el código ISO (y el signo igual):

Este es el contenido del archivo "select2.txt":

Contenido del select2.txt
AR=BA:Buenos Aires*CB:Córdoba*ER:Entre Ríos
MX=DF:Distrito Federal*MI:Michoacán*MY:Monterrey
CO=DC:Distrito Capital*AT:Atlántico*AN:Antioquía

Y esta es la función:

Función GetContentSel2()
function GetContentSel2($sel) {
  $result = "";
  $found = FALSE;
  $fh = fopen("select2.txt","r");
  do {
    $aux = trim(fgets($fh));
    $aux = explode("=",$aux);
    if ($aux[0] == $sel) {
       $found = TRUE;
       $result = $aux[1];
    }
  } while (($found == FALSE) and (!feof($fh));
  fclose($fh);
  return $result;
}

La función espera como parámetro un código ISO de país de dos letras. Defino una variable vacía que será la que usaré como valor devuelto por la función, defino una variable bandera que me indicará si encontré o no el código dentro del archivo. Abro el archivo en modo lectura (el archivo debe existir!), inicio un cliclo do .. while, leo una línea de texto (la función PHP trim() me elimina aquí el caracter de fin de línea ya que ese caracter puede causar problemas de formato más adelante en el código HTML). Aplico explode() a la cadena leída la cual divide la cadena en el signo "=" resultando en dos cadenas que van a parar a la variable $aux (esta pasa de ser una variable string a un array con índice numérico), en la posición cero del array tengo el codigo ISO de país, el cual comparo con el parámetro de la función, en caso de ser igual, establezco a TRUE la variable que me indica que he encontrado el valor y asigno la variable de resultado con la segunda parte de la cadena (la que queda a la derecha del signo "="). Todo esto se repite hasta que o bién encontré lo que estaba buscando o bién llegué al final del archivo. Cierro el archivo y devuelvo el resultado.

¿Cómo sé que esta función encontró lo que estaba buscando?, porque en caso de no encontrar el país dentro del archivo devuelve una cadena vacía. La parte del código relevante queda como sigue:

Usando la función GetContentSel2()

$fillsel2 = FALSE; // esta es la variable bandera
$sel1 = ""; // esta variable debe estar definida
$request_method = $_SERVER["REQUEST_METHOD"];
if ($request_method == "POST") {
  $sel1 = @$_POST['selector1'];
  if (!empty($sel1) and ($sel1 != "null")) {
    $contentsel2 = GetContentSel2($sel1);
if (!empty($contentsel2)) { $fillsel2 = TRUE; }
} } // if reqmet ?>

El tinglado toma forma.

La variable $contentsel2 contendrá la línea con los datos para rellenar el select "selector2". El rellenado lo hago de la siguiente manera:

Armando el select "selector2"
  

Como habíamos visto antes, la variable $fillsel2 es la que me indica si debo o no debo llenar el select "selector2", uso explode para dividir la cadena $contentsel2 que es la que contiene la línea con las províncias del país, uso la estructura de control del lenguaje PHP foreach para recorrer el array resultante, cada posición del ahora array $contentsel2 debo dividirla a su vez en código de provincia y su nombre, que es lo que hago dentro del ciclo foreach devolviendo el array $item. Con los valores de $item escribo las cláusulas

Con esto tenemos nuestro problema resuelto. Cada vez que el usuario selecciona un valor en el select "selector1", el select "selector2" se carga con los valores correspondiendes al país seleccionado.

Hay un pequeño problema.

Pero, si el lector ha probado por su cuenta el código expuesto hasta aquí, habrá notado un pequeño inconveniente: cuando el usuario selecciona un país, la página se recarga, el select "selector2" toma los valores correctos, pero el select "selector1" regresa al valor por omisión sin importar qué país seleccionó previamente lo que puede ser confuso para el usuario. Esto se debe a la cláusula "selected" del tag "option" y si esa cláusula no está presente automáticamente muestra el primero. Sin embargo podemos usar esa cláusula a nuestro favor. Para ello usaremos la variable $sel1 que, si miran el código PHP al inicio del archivo, yo había señalado que esa variable debía estar definida. La variable $sel1 contiene el valor seleccionado previamente en el select "selector1" o ningún valor en caso que sea la primera vez que se carga la página. Con esta información es facil darse cuenta lo que hay que hacer: simplemente preguntar si $sel1 vale lo mismo que el valor correspondiente en cada

Arreglando el problema
  

La solución.

Ahora sí, el código completo:

La solución completa

function GetContentSel2($sel) {
  $result = "";
  $found = FALSE;
  $fh = fopen("select2.txt","r");
  do {
    $aux = trim(fgets($fh));
    $aux = explode("=",$aux);
    if ($aux[0] == $sel) {
      $found = TRUE;
      $result = $aux[1];
    }
  } while (($found == FALSE) and (!feof($fh)));
  fclose($fh);
  return $result;
}
$fillsel2 = FALSE;
$sel1 = "";
$request_method = $_SERVER["REQUEST_METHOD"];
if ($request_method == "POST") {
  $sel1 = @$_POST['selector1'];
  if (!empty($sel1) and ($sel1 != "null")) {
    $contentsel2 = GetContentSel2($sel1);
	if (!empty($contentsel2)) {	$fillsel2 = TRUE; }
  }
} // if reqmet
?>


Ejemplo de select dependientes




El ejemplo funcionando puede ser probado aquí.

Algunas preguntas que pueden surgir:

¿Qué pasa cuando el usuario hace click en "Enviar"?.

Pues que los datos del formulario van a parar al script "recibe.php".

¿Pero y si el usuario no seleccionó nada?.

En ese caso en el script "recibe.php" tendrás que verificar que los datos sean correctos. El mecanismo implementado acá no garantiza que los datos sean correctos, es simplemente para hacer la interfaz más amigable al usuario, más intuitiva. Aunque sí lleva implícita cierta validación, en el sentido de que previene que en el segundo select haya valores que no se correspondan con lo que dice el primero. Pero aún así siempre se debe tener en cuenta que "nunca debe confiarse en los datos que proporciona el usuario".

¿De dónde salen los valores del primer select?.

En el ejemplo que expongo en este artículo esos valores están "hardcodeados", pero no veo problema en que esos valores se carguen desde una base de datos también, las condiciones para hacerlo están implícitas en el ejemplo.

¿Se puede prescindir del botón "Enviar"?.

Yo creo que sí, basta con implementar una segunda función JavaScript que se ejecute en el evento onChange del select "selector2" muy similar a la implementada en mi ejemplo. Aunque esto a veces no es deseable porque no da oportunidad al usuario a corregir ("error de dedo" como suele decir mi socio :P).

No me gusta que la página se recargue cada vez que se selecciona un item del select "selector1".

Estonces te invito a leer la segunda parte de este artículo donde implemento una solución en AJAX que no recarga la página cuando se selecciona un item en el primer select. Además la fuente de datos es una base de datos.

Por Diego Romero,