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
<html>
<head>
<title>Ejemplo de select dependientes</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
  <form name="form1" method="post" action="recibe.php">
    <select name="selector1" id="selector1">
      <option value="null" selected>Seleccione un valor</option>
      <option value="AR">Argentina</option>
      <option value="MX">M&eacute;xico</option>
      <option value="CO">Colombia</option>
    </select>
    <select name="selector2" id="selector2">
      <option value="null" selected>(Vac&iacute;o)</option>
    </select>
    <input type="submit" name="Submit" value="Enviar">
  </form>
</body>
</html>

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
<select name="selector1" id="selector1" onChange="javascript:Seleccionar();">

Luego escribo la función JavaScript "Seleccionar()", esta función debe ir entre los tags <head> en el código HTML:

función Seleccionar()
<script language="JavaScript" type="text/JavaScript">
function Seleccionar() {
  var a = document.form1.selector1.value;
  if (a != "null") {
      document.form1.action="";
	 document.form1.submit();  }
  else { alert("Seleccione un valor."); }
}
</script>

Paso a explicar lo que hace esta función. Primero asigno a la variable "a" el valor actual del select "selector1" que está en el formulario "form1" del documento (es decir, el valor que el usuario ha seleccionado). Luego comparo ese valor con la cadena "null", si es igual a ese valor, pongo un mensaje de alerta al usuario (esto puede ser quitado tranquilamente pero lo pongo aquí para que el lector vea cómo funciona esta función). Si el valor es distinto de "null" procedo a cambiar el "action" de "form1" para que se llame a sí mismo y provoco el envio del formulario. El envio se hace usando el método (method) que está especificado en "form1", es decir "POST".

Aquí me baso en una asunción y es que la primera vez que se carga esta página HTML el navegador usa el método GET, pero cuando la carga la provoca esta función JavaScript usará el método POST, esto me servirá para que, mediante PHP, determinar si debo tener en cuenta el valor del primer select.

PHP también ayuda.

Efectivamente, cuando esta función recarga la página lo hace enviando los datos actuales del formulario y como esta función hace el submit si y solo si se ha seleccionado un país en el primer select, ese dato puedo capturarlo con PHP para cargar el segundo select.

En PHP hay una variable superglobal del servidor que sirve para determinar el método usado por el navegador cuando solicita una página: $_SERVER["REQUEST_METHOD"]; que vale "GET" o "POST" entre otros. Al mismo tiempo el navegador envia los datos actuales del formulario. En PHP se pueden recuperar esos valores accediento a la variable superglobal $_POST que es un array donde cada posición del array tiene como índice el nombre del elemento HTML ("selector1", "selector2", "Submit") y el valor de cada posición es el valor ("value") de ese elemento. Con estos dos datos ya puedo determinar qué valor en el select "selector1" ha seleccionado el usuario.

tomar los datos del formulario (incompleto)
<?php
$request_method = $_SERVER["REQUEST_METHOD"];
if ($request_method == "POST") {
    $sel1 = @$_POST['selector1'];
    if (!empty($sel1) and ($sel1 != "null")) {
    // Recuperar valores para el select "selector2"  }
} // if reqmet
?>

El código anterior está incompleto por varias razones que comentaré más adelante. Lo que hace este código es tomar el tipo de petición, si la petición es "POST" extraigo el valor de "selector1" del array $_POST (el "@" delante de $_POST previene que el script salte en error en caso de que no exista el índice "selector1" en el array, si ese es el caso, la variable a la izquierda del "=" queda definida pero vacía), luego pregunto si la variable $sel1 no está vacía y no vale "null" para asegurarme (mínimamente eso sí) de que contiene un valor válido, en caso de que estas condiciones sean ciertas procedería a recuperar la lista de valores que contendrá el select "selector2". Este script PHP debe ser lo primero en ejecutarse al cargarse la página, es decir, debe estar encima del tag <html> de la página como se verá más adelante.

Ya estaríamos en condiciones de rellenar el segundo select pero aún así necesitamos una indicación de que efectivamente se han cargado esos valores, puesto que si no se hizo, como en el caso de que la página se cargue la primera vez, hay que armar el select "por omisión" tal como está en el HTML de más arriba. Haré uso de una variable bandera tipo booleana. El código a continuación integra todo lo visto hasta ahora:

El código va tomando forma (incompleta)
<?php
$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")) {
      // Recuperar valores para el select "selector2"
	  $fillsel2 = TRUE;  }
  } // if reqmet
?>
<html>
<head>
<title>Ejemplo de select dependientes</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<script language="JavaScript" type="text/JavaScript">
function Seleccionar() {
  var a = document.form1.selector1.value;
  if (a != "null") {
      document.form1.action="";
	 document.form1.submit();  }
  else { alert("Seleccione un valor."); }
}
</script>
<body>
<form name="form1" method="post" action="recibe.php">
  <select name="selector1" id="selector1" onChange="javascript:Seleccionar();">
    <option value="null" selected>Seleccione un valor</option>
    <option value="AR">Argentina</option>
    <option value="MX">M&eacute;xico</option>
    <option value="CO">Colombia</option>
  </select>
  <select name="selector2" id="selector2">
<?php
   if ($fillsel2) {
  // Aquí se arma el select con los valores recuperados
  }
  else { // continua el option por omisión
?>
    <option value="null" selected>(Vac&iacute;o)</option>
<?php } ?>
  </select>
  <input type="submit" name="Submit" value="Enviar">
</form>
</body>
</html>

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()
<?php
$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"
  <select name="selector2" id="selector2">
<?php
  if ($fillsel2) {
      $contentsel2 = explode("*",$contentsel2);
      foreach($contentsel2 as $key => $value) {
        $item = explode(":",$value);
        echo '<option value="'.$item[0].'">'.$item[1].'</option>'."\n";
      } // foreach
  } // if
  else {
?>
        <option value="null" selected>(Vac&iacute;o)</option>
<?php } ?>
  </select>

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 <option> correspondientes.

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 <option>:

Arreglando el problema
  <select name="selector1" id="selector1" onChange="javascript:Seleccionar();">
    <option value="null"<?php if (empty($sel1)) { echo " selected"; }
 ?>>Seleccione un valor</option>
    <option value="AR"<?php if ($sel1 == "AR") { echo " selected"; }
 ?>>Argentina</option>
    <option value="MX"<?php if ($sel1 == "MX") { echo " selected"; }
 ?>>México</option>
    <option value="CO"<?php if ($sel1 == "CO") { echo " selected"; }
 ?>>Colombia</option>
  </select>

La solución.

Ahora sí, el código completo:

La solución completa

<?php
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
?>
<html>
<head>
<title>Ejemplo de select dependientes</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<script language="JavaScript" type="text/JavaScript">
function Seleccionar() {
  var a = document.form1.selector1.value;
  if (a != "null") { 
     document.form1.action="";
	 document.form1.submit();
  }
  else { alert("Seleccione un valor."); }
}
</script>
<body>
<form name="form1" method="post" action="recibe.php">
  <select name="selector1" id="selector1" onChange="javascript:Seleccionar();">
    <option value="null"<?php if (empty($sel1)) { echo " selected"; }
    ?>>Seleccione un valor</option>
    <option value="AR"<?php if ($sel1 == "AR") { echo " selected"; }
    ?>>Argentina</option>
    <option value="MX"<?php if ($sel1 == "MX") { echo " selected"; }
    ?>>México</option>
    <option value="CO"<?php if ($sel1 == "CO") { echo " selected"; }
    ?>>Colombia</option>
  </select>
  <select name="selector2" id="selector2">
<?php
  if ($fillsel2) {
    $contentsel2 = explode("*",$contentsel2);
    foreach($contentsel2 as $key => $value) {
	  $item = explode(":",$value);
	  echo '<option value="'.$item[0].'">'.$item[1].'</option>'."\n";
    } // foreach
  } // if
  else {
?>
    <option value="null" selected>(Vacío)</option>
<?php } ?>
  </select>
  <input type="submit" name="Submit" value="Enviar">
</form>
</body>
</html>

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,