Repaso general de la solución GrailsVistas, Controllers, Modelos.
Inicio del taller: Conversión millas a kilómetrosPrimera versión en JavascriptVista - HTML pelado con un control texto y un botón convertirEn el botón llamamos a una función que convierte y que cargue el resultado en un html Creamos un proyecto Grails pero ojo, el html puro hay que ponerlo en web-app <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Conversor</title> </head> <body> <span>Ingrese las millas</span> <input type="number" id="millas" name="millas"/> <input type="button" value="Convertir" onclick="javascript:convertir();"/> <div id="resultado"> </div> </body> <script> function convertir() { var millas = document.getElementById('millas').value; var kilometros = 1.609344 * millas; document.getElementById('resultado').innerHTML = 'Equivalen a ' + kilometros + ' kms.'; }</script> Segunda versión: historial de conversionesCreando dinámicamente divs, vamos obteniendo un histórico de las conversiones que fuimos teniendo:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Conversor</title> </head> <body> <span>Ingrese las millas</span> <input type="number" id="millas" name="millas"/> <input type="button" value="Convertir" onclick="javascript:convertir();"/> <div id="resultados"> </div> </body> </html> <script> function convertir() { var millas = document.getElementById('millas').value; var kilometros = 1.609344 * millas; var nuevoSpan = document.createElement('div'); nuevoSpan.innerHTML = millas + ' equivalen a ' + kilometros + ' kms.'; document.getElementById('resultados').appendChild(nuevoSpan); } </script></script> Ah claro, esto era en Grails :^P ¿Entonces? Primer conversor en GrailsVamos a hacer un Conversor en Grails. Diseñamos sobre el mismo html pelado:Controller: recibe un string, lo convierte a número, lo multiplica por 1.609344 y hace un render del mismo index.gsp con el label/div/span/p. Ah, hay que agregarlo, ya no podemos crearlo nosotros. La vista cambia, de html ahora pasa a ser gsp porque tengo que ponerle los tags de grails. New Controller > ar.edu.conversor.ConversorMillasAKm y copiamos primerConversor.html de web-app/conversor a views/conversorMillasAKm/index.gsp
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Conversor</title> </head> <body> <form method="post"> <span>Ingrese las millas</span> <input type="number" id="millas" name="millas"/> <g:actionSubmit value="Convertir" action="convertir"/> <div id="resultados"> ${resultado} </div> </form> </body> </html> En el controller hacemos una conversión sin que aparezca un modelo: package ar.edu.conversor class ConversorMillasAKmController { def index() { } def convertir() { BigDecimal millas = new BigDecimal(params.millas) BigDecimal resultado = millas * 1.609344 render(view: 'index', model: [resultado: resultado]) } } Conversor Onzas a GramosAhora necesitamos un conversor de onzas a gramos (29.5735296875).
Y dos vistas, que son copy & paste. Mmmm... muy choto. Pero probémoslo, y asegurémonos de que ambas conversiones anden ok. New Controller > ar.edu.conversor.ConversorOnzasAGramos class ConversorOnzasAGramosController { def index() { } def convertir() { BigDecimal onzas = new BigDecimal(params.onzas) BigDecimal resultado = onzas * 29.5735296875 render(view: 'index', model: [resultado: resultado]) } } Y copiamos el gsp y cambiamos los labels: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Conversor</title> </head> <body> <form method="post"> <span>Ingrese las onzas</span> <input type="number" id="onzas" name="onzas"/> <g:actionSubmit value="Convertir" action="convertir"/> <div id="resultados"> ${resultado} </div> </form> </body> </html> Un refactor generalAparece el modeloVemos lo que se repite. Además ya a esta altura parece piola abstraer un modelo:
Los modelos los creamos con New Groovy Class en src/groovy package ar.edu.conversor class Conversor { String unidadMedidaOrigen BigDecimal valorOrigen String unidadMedidaDestino BigDecimal valorDestino BigDecimal factorConversion def convertir() { valorDestino = valorOrigen * factorConversion } } Un controller abstractoy generamos un AbstractConversorController.Lo que cambia es el modelo: manejamos con instancias cada conversión porque lo único que cambia es... el factor de conversión. Marche un template method. package ar.edu.conversor abstract class AbstractConversorController { def index() { } def convertir() { def conversor = getConversorOrigen() conversor.valorOrigen = new BigDecimal(params.valorOrigen) conversor.convertir() render(view: 'index', model: [conversor: conversor]) } } package ar.edu.conversor class ConversorMillasAKmController extends AbstractConversorController { def getConversorOrigen() { new Conversor(unidadMedidaOrigen: 'millas', unidadMedidaDestino: 'kilómetros', factorConversion: 1.609344) } } package ar.edu.conversor class ConversorOnzasAGramosController extends AbstractConversorController { def getConversorOrigen() { new Conversor(unidadMedidaOrigen: 'onzas', unidadMedidaDestino: 'gramos', factorConversion: 29.5735296875) } } <Recreo intermedio> Seguimos en donde hayamos cortado, y lo que sigue es presentar el template, para eliminar la duplicación de la vista. Creamos un directorio conversor y usamos un _formConversor.gsp para ambos casos de uso. El template es un gsp, pero está pensado justamente para ser reutilizado en más de un contexto:
Sí, tiene que ir con underscore. El AbstractConversorController hace un render de ese index, que va a ser bien general. Veamos el gsp: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Conversor</title> </head> <body> <form method="post"> <span>Ingrese ${conversor.unidadMedidaOrigen}</span> <input type="number" name="valorOrigen" value="${conversor.valorOrigen}"/> <g:actionSubmit value="Convertir" action="convertir"/> <g:if test="${conversor.valorDestino}"> <div id="resultados"> ${conversor.valorDestino} ${conversor.unidadMedidaDestino} </div> </g:if> </form> </body> </html> Ah, claro, se reemplazan los labels por lo que nos diga el conversor. También ponemos un g:if para no mostrar el div al inicio. Lo corremos y... revienta!!! 1) no hay más index 2) el index no manda el conversor... entonces esto tampoco va a andar: package ar.edu.conversor abstract class AbstractConversorController { def index() { render(template: '/conversor/formConversor') } def convertir() { def conversor = getConversorOrigen() conversor.valorOrigen = new BigDecimal(params.valorOrigen) conversor.convertir() render(template: '/conversor/formConversor', model: [conversor: conversor]) } } En el index tenemos que hacer: def index() { render(template: '/conversor/formConversor', model: [conversor: getConversorOrigen()]) } Conversor Celsius a FahrenheitMetemos el conversor Celsius a Fahrenheit: fahrenheit = (celsius * 9.0) / 5.0 + 32Creamos un nuevo dominio polimórfico, una subclase nueva del conversor y todo funca ok New Groovy Class en el mismo package del conversor package ar.edu.conversor class ConversorCelsiusAFahrenheit { def getUnidadMedidaOrigen() { "Celsius" } def getUnidadMedidaDestino() { "Fahrenheit" } BigDecimal valorOrigen BigDecimal valorDestino def convertir() { valorDestino = (valorOrigen * 9.0) / 5.0 + 32 } } Y New Groovy Class desde la solapa de Controllers (no vamos a crear el controller porque no nos interesa generar el gsp en un directorio) package ar.edu.conversor class ConversorCelsiusAFahrenheitController extends AbstractConversorController { def getConversorOrigen() { new ConversorCelsiusAFahrenheit() } } Restarteamos y probamos Manejo del errorComo es un input type number lo maneja el cliente.Pero si pasamos a text... nos tira un error. Vamos a capturar ese error. Lo bueno... lo hacemos en un solo lugar En el controller def convertir() { def conversor = getConversorOrigen() String mensaje = "" try { conversor.valorOrigen = new BigDecimal(params.valorOrigen) conversor.convertir() } catch (NumberFormatException e) { mensaje = params.valorOrigen + " no es un número válido" } render(template: '/conversor/formConversor', model: [conversor: conversor, mensajeError: mensaje]) } Mostramos un mensaje de error con un div + g:if <body> <g:if test="${mensajeError}"> <div>${mensajeError}</div> </g:if> <form method="post"> <span>Ingrese ${conversor.unidadMedidaOrigen}</span> <input type="text" name="valorOrigen" value="${conversor.valorOrigen}"/> Ejemplo de un TaglibArmamos un taglib sencillo, que va a mostrar
package ar.edu import java.text.NumberFormat class NumericTagLib { static namespace = "a3" static defaultEncodeAs = [taglib:'html'] def numero = { attrs, body -> def cantidadDecimales = attrs.cantidadDecimales.toInteger() ?: 3 def formatoLocal = NumberFormat.getInstance(new Locale("es")) formatoLocal.setMinimumFractionDigits(cantidadDecimales) formatoLocal.setMaximumFractionDigits(cantidadDecimales) out << formatoLocal.format(attrs.valor) } } Y en nuestro gsp lo usamos como cualquier otro tag: <div id="resultados"> <a3:numero valor="${conversor.valorDestino}" cantidadDecimales="2"/> ${conversor.unidadMedidaDestino} </div> |