En un artículo anterior en esta serie aprendimos cómo obtener datos que no estaban a la vista al renderizar las tablas, pero que podían visualizarse, en una fila desplegable, pulsando sobre un icono. En este artículo vamos a ir un paso más allá. Veremos cómo insertar imágenes en una fila desplegable. Además, seguiremos aprendiendo sobre la adaptación previa de los datos, antes de la renderización, que ya vimos, también, en otro artículo.
Este artículo presenta, cómo tales, pocas novedades. Sin embargo, a la hora de reforzar algunos conocimientos, y explotar mejor las posibilidades que ya conocemos, tiene un gran valor didáctico. Yo mismo he aprendido cosas redactándolo, y he disfrutado de ellas. Espero que este artículo sirva para compartir contigo ambas experiencias.
EL ESCENARIO
El escenario sobre el que vamos a trabajar es una base de datos de usuarios, cada uno de los cuales, además de sus propios datos, puede tener (o no tener), una o más imágenes asociadas. Podría ser la base de una rudimentaria red social, o un portal de contactos, o alguna otra estructura similar. Para las imágenes, no emplearemos fotos de personas, sino imágenes obtenidas en la Red, libres de derechos. En Internet existen gran cantidad de sitios donde se puede encontrar este tipo de material.
Nuestro escenario cuenta con una base de datos con dos tablas: una con los datos de los usuarios. Otra con los nombres de las imágenes, y una columna con el id del usuario al que pertenece cada una. Todo el materíal (base de datos, scripts e imágenes) te lo he dejado para descargar en este enlace.
La tabla de usuarios sólo cuenta con los campos relativos al nombre, apellidos, fecha de ingreso en el portal, y su género (masculino o femenino), además, por supuesto, del identificador autoincrementable de la tabla. Si bien para un portal real estos datos serían insuficientes, para este artículo cubren nuestras necesidades, y no quiero dispersarme en contenidos innecesarios.
La tabla de imágenes tiene el nombre del archivo de imagen, el id del usuario al que pertenece, y su propio id.
Un pequeño inciso. Cuando crees tablas en MySQL ponles siempre una clave primaria autoincrementable, incluso si, al modelar los datos, piensas que no es necesaria. Esta cumple dos misiones de vital importancia. Puede ser un campo para relacionar tu tabla con otra. Esta función no siempre existe. Por ejemplo, en nuestro escenario, en la tabla de usuarios sí existe, ya que se usa el id para relacionar a los usuarios con las imágenes. En la tabla de imágenes, en cambio, el id de cada imagen no se relaciona con nada. La otra función es que una clave primaria autoincrementable constituye un índice muy eficiente y rápido cuando queremos obtener los datos de una tabla sin usar un criterio de ordenación específico. Además, en ciertos entornos de trabajo (como phpMyAdmin), si no existe esta clave, al examinar la tabla no se pueden borrar o editar registros. Por cierto. Estas claves primarias, que no son parte de los datos propios de cada registro, y que sólo (?) cumplen la función de ser claves primarias se conocen, en el mundillo SQL, con el nombre de claves subrogadas. |
Y vale ya de charla. Vamos al tajo.
EL SCRIPT PRIMARIO
Este script presenta importantes detalles (y alguna novedad) que vamos a conocer. Lo he llamado artículo_14.php
y su listado es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Uso de DataTables</title> <!-- El CSS de DataTables --> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/jszip-2.5.0/pdfmake-0.1.18/dt-1.10.12/af-2.1.2/b-1.2.2/b-colvis-1.2.2/b-flash-1.2.2/b-html5-1.2.2/b-print-1.2.2/cr-1.3.2/fc-3.2.2/fh-3.1.2/kt-2.1.3/r-2.1.0/rr-1.1.2/sc-1.4.2/se-1.2.0/datatables.min.css"/> <!-- El CSS de Bootstrap --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <!-- Font Awesome --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> </head> <body> <br> <div class="container"> <div class="row"> <div class="col-sm-12"> <table id="tabla_de_personal" class="table display table-striped table-bordered" width="100%"> <thead> <tr> <th> </th> <th>NOMBRE Y APELLIDOS</th> <th>F. ALTA</th> <th>GENERO</th> </tr> </thead> <tbody> </tbody> </table> </div> </div> </div> <!-- jQuery --> <script language="javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <!-- El JavaScript de DataTables --> <script language="javascript" type="text/javascript" src="https://cdn.datatables.net/v/dt/jszip-2.5.0/pdfmake-0.1.18/dt-1.10.12/af-2.1.2/b-1.2.2/b-colvis-1.2.2/b-flash-1.2.2/b-html5-1.2.2/b-print-1.2.2/cr-1.3.2/fc-3.2.2/fh-3.1.2/kt-2.1.3/r-2.1.0/rr-1.1.2/sc-1.4.2/se-1.2.0/datatables.min.js"></script> <!-- El JavaScript de BootStrap --> <script language="javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <!-- El código JavaScript --> <script language="javascript"> /* SE DEFINE EL OBJETO DATATABLES. SU USO ES, PRÁCTICAMENTE, EL MISMO QUE SIEMPRE, PARA RECUPERAR UN DATASET Y RENDERIZARLO EN LA PÁGINA. */ var objetoDataTables_personal = $('#tabla_de_personal').DataTable({ "language": { "emptyTable": "No hay datos disponibles en la tabla.", "info": "Del _START_ al _END_ de _TOTAL_ ", "infoEmpty": "Mostrando 0 registros de un total de 0.", "infoFiltered": "(filtrados de un total de _MAX_ registros)", "infoPostFix": "(actualizados)", "lengthMenu": "Mostrar _MENU_ registros", "loadingRecords": "Cargando...", "processing": "Procesando...", "search": "Buscar:", "searchPlaceholder": "Dato para buscar", "zeroRecords": "No se han encontrado coincidencias.", "paginate": { "first": "Primera", "last": "Última", "next": "Siguiente", "previous": "Anterior" }, "aria": { "sortAscending": "Ordenación ascendente", "sortDescending": "Ordenación descendente" } }, "lengthMenu": [[5, 10, 20, 25, 50, -1], [5, 10, 20, 25, 50, "Todos"]], "iDisplayLength": 10, "bServerSide": true, "sAjaxSource": "datos_externos_14.php", // OJO. Ver el código para comprender. /* Definimos la operativa previa necesaria para dos columnas, de modo que se muestren de una forma "amigable", en lugar de los datos "en bruto". En primer lugar, en la columan que correesponde al nombre se muestra el nombre y los apellidos. En segundo lugar, en la columna que corresponde al género, como en la base de datos este aparece como M o F, se muestra "Masculino" o "Femenino".*/ "columnDefs": [ { "targets": 1, "render": function (data, type, row) { return row['nombre']+' '+row['apellidos']; } }, { "targets": 3, "render": function (data, type, row) { return (data == 'M')?"Masculino":"Femenino"; } } ], "columns" : [ /* Se define la columna que albergará iconos para mostrar una fila desplegable por cada registro. */ { "className": 'celda_de_descripcion', "orderable": false, "data": null, "defaultContent": '<div class="text-center fa fa-plus-circle" style="width:100%; color: #3dc728;"></div>' }, {"data": 'nombre'}, {"data": 'fecha_alta'}, {"data": 'genero'} ], "order": [1, 'asc'] }); $('label').addClass('form-inline'); $('select, input[type="search"]').addClass('form-control input-sm'); $('#tabla_de_personal tbody').on('click', 'td.celda_de_descripcion', function () { var filaDeLaTabla = $(this).closest('tr'); var filaComplementaria = objetoDataTables_personal.row(filaDeLaTabla); var celdaDeIcono = $(this).closest('td.celda_de_descripcion'); if (filaComplementaria.child.isShown() ) { // La fila complementaria está abierta y se cierra. filaComplementaria.child.hide(); celdaDeIcono.html('<div class="text-center fa fa-plus-circle" style="width:100%; color: #3dc728;"></div>'); } else { // La fila complementaria está cerrada y se abre. filaComplementaria.child(formatearSalidaDeDatosComplementarios(filaComplementaria.data(), 'nombres_imagenes')).show(); celdaDeIcono.html('<div class="text-center fa fa-minus-circle" style="width:100%; color: #e80909;"></div>'); } }); /* La función que despliega la fila de contenidos adicionales detecta si el usuario tienen imágenes, en ese caso, las muestra en una tabla. Lo suyo sería mostrarlas en un flexbox, con funcionalidades de zoom, etc, pero para este ejemplo ya vemos cómo usarlo. Luego, es darle todo el toque personal que queramos. */ function formatearSalidaDeDatosComplementarios (filaDelDataSet, columna) { var cadenaDeRetorno = ''; cadenaDeRetorno += '<div style="overflow-x:auto; overflow-y:visible;">'; if (filaDelDataSet[columna].length == 0){ cadenaDeRetorno += 'Este usuario no tiene imágenes disponibles'; } else { cadenaDeRetorno += '<table border="0" cellspacing="10">'; cadenaDeRetorno += '<tr>'; for (imagen of filaDelDataSet[columna]){ cadenaDeRetorno += '<td align="center" valign="top">'; cadenaDeRetorno += '<img src="imagenes_14/miniaturas/'; cadenaDeRetorno += imagen + '" border="0">'; cadenaDeRetorno += '</td>'; } cadenaDeRetorno += '</tr>'; cadenaDeRetorno += '</table>'; } cadenaDeRetorno += '</div>'; return cadenaDeRetorno; } </script> </body> </html> |
Lo primero que nos llama la atención es lo que hay en el primer bloque resaltado, entre las líneas 81
y 93
. El uso de columnDefs
en sí no es ya nuevo para nosotros. Es la forma en que lo hemos empleado, permitiéndonos el procesado de dos columnas, previo a la renderización, lo que quizá nos llame un poco la atención. Por un lado, en la columna del nombre incluimos este junto a los apellidos. Por otra parte, el género, que en la base de datos está como M
o F
lo hacemos aparecer cómo Masculino
o Femenino
. De este modo logramos un resultado más amigable al usuario.
ATENCIÓN. Si quieres comprender realmente cómo funciona render en
a esto otro:
Y lo mismo haríamos con el bloque entre las líneas Ahora, antes de cargar el script en el navegador, despliega la consola de JavaScript ( |
Por otra parte, fíjate en el segundo bloque resaltado, entre las líneas 130
y 149
. En realidad, con los conocimientos de JavaScript que tiene cualquier usuario medio, enseguida ves lo que hacemos. Si el usuario tiene imágenes, se muestran las que hay en la carpeta miniaturas en una tabla. Si no, se muestra un literal notificando este hecho.
En realidad, este script no tiene novedades dignas de mención. Con lo que hemos aprendido en los artículos anteriores y los comentarios del código, no creo que haya lugar a ninguna duda.
EL SCRIPT SECUNDARIO
Este script presenta alguna curiosidad que nos llama la atención. El listado se llama datos_externos_14.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
<?php // Establecemos la codificacion para las llamadas y respuestas HTTP mb_internal_encoding ('UTF-8'); /* CREAMOS LA CONEXION A LA BASE DE DATOS, O BIEN LA IMPORTAMOS DESDE UN ARCHIVO EXTERNO DE CONFIGURACION. */ $conexion = new PDO('mysql:host=localhost;dbname=datatables_con_imagenes;charset=UTF8', 'root', ''); $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* RECUPERAMOS TODOS LOS PARAMETROS DE $_GET. LOS QUE NO APAREZCAN EN LA CONSULTA RECIBIRAN UN VALOR PREDETERMINADO. ESTOS DATOS SON LOS QUE SE RECIBEN CADA VEZ QUE EL PLUGIN DATATABLES LLAMA A ESTE SCRIPT. */ $datosDeLlamada = $_GET; /* SE INDICA(N) LA(S) TABLA(S) QUE SE VA(N) A USAR EN LAS CONSULTAS. */ $tablasDeBBDD = array( 'usuarios', 'imagenes' ); /* SE DEFINE LA LISTA DE COLUMNAS QUE SE DEVOLVERON PARA SER MOSTRADAS EN LA TABLA HTML. LOS NOMBRES DE ESTAS COLUMNAS DEBEN COINCIDIR CON LOS DE LAS COLUMNAS DE LA(S) TABLA(S) AFECTADA(S) POR LA CONSULTA. */ $columnasParaRetorno = array( $tablasDeBBDD[0].'.id', $tablasDeBBDD[0].'.nombre', $tablasDeBBDD[0].'.fecha_alta', $tablasDeBBDD[0].'.genero', $tablasDeBBDD[0].'.apellidos' ); $numeroDeColumnas = count($columnasParaRetorno); //////////////////////////////////////////////// REGLAS DE FILTRADO //////////////////////////// /* PREPARAMOS EL FILTRADO POR COLUMNAS PARA LA CAJA DE BUSQUEDA */ $reglasDeFiltradoDeUsuario = array (); if (isset($datosDeLlamada['sSearch']) && $datosDeLlamada['sSearch'] != "") { for($i = 0; $i < $numeroDeColumnas; $i++) { if (isset ($datosDeLlamada['bSearchable_'.$i]) && $datosDeLlamada['bSearchable_'.$i] == 'true') { $reglasDeFiltradoDeUsuario[] = $columnasParaRetorno[$i]." LIKE '%".addslashes($datosDeLlamada['sSearch'])."%'"; } } } if (!empty($reglasDeFiltradoDeUsuario)){ $reglasDeFiltradoDeUsuario = ' ('.implode(" OR ", $reglasDeFiltradoDeUsuario).') '; } else { $reglasDeFiltradoDeUsuario = ''; } /* PREPARAMOS LAS REGLAS DE FILTRADO PARA LAS BUSQUEDAS PROGRAMADAS POR METODO .search() (BÚSQUEDAS PERSONALIDAS DEL USUARIO). */ $reglasDeBusquedaPorProgramacion = array(); for($i = 0; $i < $numeroDeColumnas; $i++) { if (!isset($datosDeLlamada['sSearch_'.$i])) continue; if ($datosDeLlamada['sSearch_'.$i] !== ""){ $reglasDeBusquedaPorProgramacion[] = $columnasParaRetorno[$i].$datosDeLlamada['sSearch_'.$i]; } } if (!empty($reglasDeBusquedaPorProgramacion)){ $reglasDeBusquedaPorProgramacion = ' ('.implode(" AND ", $reglasDeBusquedaPorProgramacion).') '; } else { $reglasDeBusquedaPorProgramacion = ''; } /* AQUI SE VA A PROCESAR UNA SOLA TABLA (LA DE USUARIOS), YA QUE, AUNQUE ESTÉ RELACIONADA CON LA DE IMAGENES, UN USUARIO PODRÍA NO TENER IMÁGENES ASOCIADAS, LO QUE DARÍA LUGAR, SI SE LEYERAN RELACIONADAS, A QUE PUDIERA HABER USUARIOS QUE NO APARECIERAN EN EL DATASET PREVIO. */ $reglasDeFiltradoDeRelaciones = ''; /* SE COMPONE TODA LA REGLA DE FILTRADO. EN ESTE CASO INCLUYE LAS CLAÚSULAS DE BÚSQUEDA, Y LAS RELACIONES ENTRE TABLAS. SIGUE SIENDO UN EJEMPLO SIMPLE, PERO MÁS ELABORADO QUE EL ANTERIOR. MÁS ADELANTE VEREMOS OTROS USOS. LO IMPORTANTE AQUI ES QUE, ADEMÁS DE LAS CLAUSULAS DE BÚSQUEDA (VARIABLE $reglasDeFiltradoDeUsuario, QUE PUEDEN EXISTIR O NO) TAMBIÉN HAY UNA CLAÚSULA DE RELACIONES ENTRE LAS TABLAS. SI HAY MÁS DE UNA TABLA SIEMPRE HABRÁ UNA CLAÚSULA DE RELACIONES ($reglasDeFiltradoDeRelaciones). LAS IMPLEMENTAMOS COMO UNA MATRIZ PARA PODER COMPROBAR LAS QUE EXISTEN Y LAS QUE NO, Y LUEGO LAS UNIMOS CON EL OPERADOR AND, SI HAY MÁS DE UNA CLAÚSULA DE FILTRADO. */ $reglasDeFiltrado = array(); if ($reglasDeFiltradoDeUsuario > '') $reglasDeFiltrado[] = $reglasDeFiltradoDeUsuario; if ($reglasDeFiltradoDeRelaciones > '') $reglasDeFiltrado[] = $reglasDeFiltradoDeRelaciones; if ($reglasDeBusquedaPorProgramacion > '') $reglasDeFiltrado[] = $reglasDeBusquedaPorProgramacion; $reglasDeFiltrado = implode(" AND ", $reglasDeFiltrado); //////////////////////////////////////////// FIN DE REGLAS DE FILTRADO /////////////////////////// /////////////////////////// REGLAS DE ORDENACION //////////////////////// /* SE COMPONE LA REGLA DE ORDENACION. */ $reglasDeOrdenacion = array (); if (isset($datosDeLlamada['iSortCol_0'] )) { $columnasDeOrdenacion = intval($datosDeLlamada['iSortingCols']); for($i = 0; $i < $columnasDeOrdenacion; $i ++) { if ($datosDeLlamada['bSortable_'.intval($datosDeLlamada['iSortCol_'.$i])] == 'true') { $reglasDeOrdenacion[] = $columnasParaRetorno[intval($datosDeLlamada['iSortCol_'.$i])].($datosDeLlamada['sSortDir_'.$i] === 'asc'?' asc':' desc'); } } } if (!empty($reglasDeOrdenacion)) { $reglasDeOrdenacion = " ORDER BY ".implode(", ", $reglasDeOrdenacion); } else { $reglasDeOrdenacion = ""; } /* SE COMPONE LA REGLA DE LIMITACION DE REGISTROS. */ $reglaDeLimitacion = ' LIMIT '.$datosDeLlamada['iDisplayStart'].', '.$datosDeLlamada['iDisplayLength'].';'; /////////////////////////////////////// FIN DE REGLAS DE ORDENACION //////////////////// /* SE PREPARA LA CONSULTA DE RECUPERACION DEL DATASET SOLICITADO. */ $consulta = "SELECT ".implode(', ', $columnasParaRetorno)." "; $consulta .= "FROM ".$tablasDeBBDD[0]." "; $consulta .= "WHERE 1 "; if ($reglasDeFiltrado > "") $consulta .= "AND (".$reglasDeFiltrado.") "; $consulta .= $reglasDeOrdenacion; $consulta .= $reglaDeLimitacion; $hacerConsulta = $conexion->prepare($consulta); $hacerConsulta->execute(); $DataSetPrevio = $hacerConsulta->fetchAll(PDO::FETCH_ASSOC); $hacerConsulta->closeCursor(); /* Se procesa el dataset previo, obtenido de la consulta, para obtener el dataset definitivo, con los datos separados en submatrices, dependiendo de la tabla de la que proceden. */ $DataSet = array(); foreach ($DataSetPrevio as $item){ $empleado = array( 'nombre'=>$item['nombre'], 'apellidos'=>$item['apellidos'], 'genero'=>$item['genero'], 'fecha_alta'=>date("d-m-Y", strtotime($item['fecha_alta'])), 'id'=>$item['id'], 'ids_imagenes'=>array(), 'nombres_imagenes'=>array() ); $DataSet[] = $empleado; unset($empleado); } unset($DataSetPrevio); /* LEEMOS LA LISTA DE IMAGENES DE CADA USUARIO. */ foreach ($DataSet as $keyUsuario=>$usuario){ $consulta = "SELECT "; $consulta .= $tablasDeBBDD[1].".id, "; $consulta .= $tablasDeBBDD[1].".nombre_imagen "; $consulta .= "FROM "; $consulta .= $tablasDeBBDD[1]." "; $consulta .= "WHERE ".$tablasDeBBDD[1].".id_usuario = '".$usuario['id']."' "; $consulta .= "ORDER BY ".$tablasDeBBDD[1].".nombre_imagen;"; $hacerConsulta = $conexion->query($consulta); $matrizDeImagenes = $hacerConsulta->fetchAll(PDO::FETCH_ASSOC); $hacerConsulta->closeCursor(); foreach ($matrizDeImagenes as $img){ $DataSet[$keyUsuario]['ids_imagenes'][] = $img['id']; $DataSet[$keyUsuario]['nombres_imagenes'][] = $img['nombre_imagen']; } } /* SI ES NECESARIO ADAPTAR ALGUN DATO PARA PRESENTACION, SE ADAPTA AQUI. AQUÍ NO NECESITAMOS ESTA FUNCIONALIDAD, PORQUE EL ÚNICO DATO A ADAPTAR, QUE ES EL FORMATO DE LA FECHA DE INGRESO, YA LO HEMOS ADAPTADO AL CREAR EL DATASET DEFINITIVO. POR ESTA RAZÓN DEJAMOS EL BUCLE VACÍO Y ANULADO, SOLO A EFECTOS DOCUMENTALES.*/ // foreach ($DataSet as $keyDL=>$DL){ // } /* CALCULO DE DATOS INFORMATIVOS DE REGISTROS. */ $numeroDeRegistrosDelDataSet = count($DataSet); /* CALCULO DEL TOTAL DE REGISTROS QUE CUMPLEN LAS REGLAS DE FILTRADO SIN ORDENACION NI LIMITACION. */ $consulta = "SELECT COUNT(".$columnasParaRetorno[0].") "; $consulta .= "FROM ".$tablasDeBBDD[0]." "; $consulta .= "WHERE 1 "; if ($reglasDeFiltrado > "") $consulta .= "AND (".$reglasDeFiltrado.") "; $hacerConsulta = $conexion->prepare($consulta); $hacerConsulta->execute(); $totalDeRegistrosConFiltrado = $hacerConsulta->fetch(PDO::FETCH_NUM)[0]; $hacerConsulta->closeCursor(); /* TOTAL DE REGISTROS DE LA TABLA PRIMARIA (O UNICA, SI SOLO HAY UNA). */ $consulta = "SELECT COUNT(".$columnasParaRetorno[0].") "; $consulta .= "FROM ".$tablasDeBBDD[0].";"; $hacerConsulta = $conexion->prepare($consulta); $hacerConsulta->execute(); $totalDeRegistrosEnBruto = $hacerConsulta->fetch(PDO::FETCH_NUM)[0]; $hacerConsulta->closeCursor(); // SE PREPARA UNA MATRIZ CON TODOS LOS DATOS NECESARIOS PARA LA SALIDA. $matrizDeSalida = array( "sEcho" => intval($datosDeLlamada['sEcho']), "iTotalRecords" => strval($totalDeRegistrosEnBruto), "iTotalDisplayRecords" => strval($totalDeRegistrosConFiltrado), "data" => array() ); foreach ($DataSet as $keyItem=>$item) $matrizDeSalida['data'][] = $item; $salidaDeDataSet = json_encode ($matrizDeSalida, JSON_HEX_QUOT); /* SE DEVUELVE LA SALIDA */ echo $salidaDeDataSet; ?> |
En lo que quiero que te fijes es en la forma en que leemos los datos. Estamos trabajando con dos tablas que están relacionadas entre sí. La tabla de usuarios, que es la que podríamos llamar principal, ya que es donde están los datos que se van a mostrar, al inicio, en la página, tiene una clave primaria llamada id
. La tabla de imágenes (secundaria) relaciona cada imagen con un usuario.
Sin embargo, al hacer la consulta, no podemos leer las dos tablas relacionadas, ya que, si por ejemplo, hay algún usuario (y los hay) que no tiene imágenes, no se recuperaría en el dataset este usuario. Así, lo que hacemos es leer la tabla de usuarios, en un dataset previo. Después, a cada usuario le leemos las imágenes que tiene (si es que las tiene), y formamos el dataset definitivo que usaremos para entregar datos al DataTables.
Esta manera de trabajar es la primera vez que la vemos, pero, desde luego, te aseguro que no será la última. De hecho, en la mayoría de escenarios reales en un dataset se ven implicadas dos o más tablas relacionadas, ya sea en 1-n o en m-n. Si lees el código, verás que, en realidad, es bastante simple. Presta especial atención a los comentarios.
LA METEDURA DE PATA
El código que hemos visto hasta ahora no está mal del todo, pero presenta un problema: como ves, no hay una columna específica para los apellidos. La consecuencia directa de esto es que no se pueden hacer búsquedas por ese campo. Si te vas a la casilla de búsquedas y tecleas un apellido que sabes que está ahí, que lo estás viendo, comprobarás que no te muestra los resultados coincidentes. Esto es porque, cómo te he comentado, no existe realmente esa columna, al haber agrupado el nombre con los apellidos.
La solución a esto es añadir una columna exclusivamente para los apellidos. La añadiremos al final de la tabla HTML, es decir, a la derecha, para que el orden de las columnas coincida con el orden de los campos de la tabla MySQL que se leen en datos_externos_14.php
. Pero claro, la columna no puede ser visible porque entonces los apellidos se verían dos veces: una en la columna del nombre completo y otra en la propia columna de los apellidos. Eso quiere decir que la columna de los apellidos tenemos que declararla como no visible.
Esto es algo que aprendimos a hacer por programación en el artículo XI, mientras que en el artículo XII veíamos como configurar una columna para que fuera invisible al inicio. Para ver como funciona, sustituye en tu ordenador el script articulo_14.php
por articulo_14_b.php
, cuyo listado es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Uso de DataTables</title> <!-- El CSS de DataTables --> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/jszip-2.5.0/pdfmake-0.1.18/dt-1.10.12/af-2.1.2/b-1.2.2/b-colvis-1.2.2/b-flash-1.2.2/b-html5-1.2.2/b-print-1.2.2/cr-1.3.2/fc-3.2.2/fh-3.1.2/kt-2.1.3/r-2.1.0/rr-1.1.2/sc-1.4.2/se-1.2.0/datatables.min.css"/> <!-- El CSS de Bootstrap --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <!-- Font Awesome --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> </head> <body> <br> <div class="container"> <div class="row"> <div class="col-sm-12"> <table id="tabla_de_personal" class="table display table-striped table-bordered" width="100%"> <thead> <tr> <th> </th> <th>NOMBRE Y APELLIDOS</th> <th>F. ALTA</th> <th>GENERO</th> <th> </th> </tr> </thead> <tbody> </tbody> </table> </div> </div> </div> <!-- jQuery --> <script language="javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <!-- El JavaScript de DataTables --> <script language="javascript" type="text/javascript" src="https://cdn.datatables.net/v/dt/jszip-2.5.0/pdfmake-0.1.18/dt-1.10.12/af-2.1.2/b-1.2.2/b-colvis-1.2.2/b-flash-1.2.2/b-html5-1.2.2/b-print-1.2.2/cr-1.3.2/fc-3.2.2/fh-3.1.2/kt-2.1.3/r-2.1.0/rr-1.1.2/sc-1.4.2/se-1.2.0/datatables.min.js"></script> <!-- El JavaScript de BootStrap --> <script language="javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <!-- El código JavaScript --> <script language="javascript"> /* SE DEFINE EL OBJETO DATATABLES. SU USO ES, PRÁCTICAMENTE, EL MISMO QUE SIEMPRE, PARA RECUPERAR UN DATASET Y RENDERIZARLO EN LA PÁGINA. */ var objetoDataTables_personal = $('#tabla_de_personal').DataTable({ "language": { "emptyTable": "No hay datos disponibles en la tabla.", "info": "Del _START_ al _END_ de _TOTAL_ ", "infoEmpty": "Mostrando 0 registros de un total de 0.", "infoFiltered": "(filtrados de un total de _MAX_ registros)", "infoPostFix": "(actualizados)", "lengthMenu": "Mostrar _MENU_ registros", "loadingRecords": "Cargando...", "processing": "Procesando...", "search": "Buscar:", "searchPlaceholder": "Dato para buscar", "zeroRecords": "No se han encontrado coincidencias.", "paginate": { "first": "Primera", "last": "Última", "next": "Siguiente", "previous": "Anterior" }, "aria": { "sortAscending": "Ordenación ascendente", "sortDescending": "Ordenación descendente" } }, "lengthMenu": [[5, 10, 20, 25, 50, -1], [5, 10, 20, 25, 50, "Todos"]], "iDisplayLength": 10, "bServerSide": true, "sAjaxSource": "datos_externos_14.php", // OJO. Ver el código para comprender. /* Definimos la operativa previa necesaria para dos columnas, de modo que se muestren de una forma "amigable", en lugar de los datos "en bruto". En primer lugar, en la columan que correesponde al nombre se muestra el nombre y los apellidos. En segundo lugar, en la columna que corresponde al género, como en la base de datos este aparece como M o F, se muestra "Masculino" o "Femenino".*/ "columnDefs": [ { "targets": 1, "render": function (data, type, row) { return data + ' ' + row['apellidos']; } }, { "targets": 3, "render": function (data, type, row) { return (data == 'M')?"Masculino":"Femenino"; } }, { "targets": 4, visible: false } ], "columns" : [ /* Se define la columna que albergará iconos para mostrar una fila desplegable por cada registro. */ { "className": 'celda_de_descripcion', "orderable": false, "data": null, "defaultContent": '<div class="text-center fa fa-plus-circle" style="width:100%; color: #3dc728;"></div>' }, {"data": 'nombre'}, {"data": 'fecha_alta'}, {"data": 'genero'}, {"data": 'apellidos'} ], "order": [1, 'asc'] }); $('label').addClass('form-inline'); $('select, input[type="search"]').addClass('form-control input-sm'); $('#tabla_de_personal tbody').on('click', 'td.celda_de_descripcion', function () { var filaDeLaTabla = $(this).closest('tr'); var filaComplementaria = objetoDataTables_personal.row(filaDeLaTabla); var celdaDeIcono = $(this).closest('td.celda_de_descripcion'); if (filaComplementaria.child.isShown() ) { // La fila complementaria está abierta y se cierra. filaComplementaria.child.hide(); celdaDeIcono.html('<div class="text-center fa fa-plus-circle" style="width:100%; color: #3dc728;"></div>'); } else { // La fila complementaria está cerrada y se abre. filaComplementaria.child(formatearSalidaDeDatosComplementarios(filaComplementaria.data(), 'nombres_imagenes')).show(); celdaDeIcono.html('<div class="text-center fa fa-minus-circle" style="width:100%; color: #e80909;"></div>'); } }); /* La función que despliega la fila de contenidos adicionales detecta si el usuario tienen imágenes, en ese caso, las muestra en una tabla. Lo suyo sería mostrarlas en un flexbox, con funcionalidades de zoom, etc, pero para este ejemplo ya vemos cómo usarlo. Luego, es darle todo el toque personal que queramos. */ function formatearSalidaDeDatosComplementarios (filaDelDataSet, columna) { var cadenaDeRetorno = ''; cadenaDeRetorno += '<div style="overflow-x:auto; overflow-y:visible;">'; if (filaDelDataSet[columna].length == 0){ cadenaDeRetorno += 'Este usuario no tiene imágenes disponibles'; } else { cadenaDeRetorno += '<table border="0" cellspacing="10">'; cadenaDeRetorno += '<tr>'; for (imagen of filaDelDataSet[columna]){ cadenaDeRetorno += '<td align="center" valign="top">'; cadenaDeRetorno += '<img src="imagenes_14/miniaturas/'; cadenaDeRetorno += imagen + '" border="0">'; cadenaDeRetorno += '</td>'; } cadenaDeRetorno += '</tr>'; cadenaDeRetorno += '</table>'; } cadenaDeRetorno += '</div>'; return cadenaDeRetorno; } </script> </body> </html> |
Observa las líneas resaltadas. En la línea 27
definimos una columna más en la cabecera de la tabla HTML. Es la que usaremos para los apellidos. No le hemos puesto rótulo, porque dado que la vamos a hacer invisible, es irrelevante, pero debe estar ahí para que el númerto de columnas de la cabecera coincida con las columnas de datos que manejaremos.
En las líneas de la 95
a la 98
es donde decimos que la columna correspondiente al campo apellidos no será visible.
En la línea 111
declaramos el dato apellidos para esta columna.
Y con eso ya está. Si ahora pones algo en la caja de búsqueda verás que lo busca en todas las columnas, incluida la de apellidos. El hecho de que sea invisible no significa que no esté ahí. Pasa un poco como con las peticiones de aumento de sueldo que se le hacen a algunos jefes: la petición está ahí, pero para el jefe es invisible. Bromas aparte, con esto vemos como agrupar campos, sin perder prestaciones o funcionalidades.
Por cierto. Esta última versión mejorada también la tienes en el paquetito que te he puesto para descargar. 😉