Material‎ > ‎Software‎ > ‎Grails‎ > ‎

Taller: reutilización en Grails


Previamente hacemos un

Repaso general de la solución Grails

Vistas, Controllers, Modelos.
  1. ¿Cómo definimos los elementos gráficos? Es lo que nos deja html + los chiches de algún fwk tipo bootstrap. Preguntar si lo usaron.
  2. ¿Cómo definimos el layout? Que cuenten un poco el css
  3. ¿Cómo funciona el binding entre vista y modelo? El binding no es bidireccional, porque tengo una restricción tecnológica: Grails es server-side y los controles los actualiza el usuario en el nodo cliente. 
  4. ¿Cómo es el pasaje de info entre páginas? La página almacena información en el form que viaja como string y lo recibe el controller, que a su vez lo convierte a los objetos que queremos para la siguiente vista.
  5. ¿Cómo es el manejo de estado de la página? Depende de si la arquitectura es stateless o stateful. En la primera no tenés estado, entonces cuando uno quiere guardar información necesitás sincronizarlo fuera de cada controller. En la segunda el controller tiene un estado y eso permite tener variables de sesión para guardar una conversación, el estado de un caso de uso.
  6. ¿Cómo se manejan los eventos? Del lado cliente hasta hoy no vimos nada, todo evento se maneja como un pedido a un server que refresca toooda la vista.

Inicio del taller: Conversión millas a kilómetros

Primera versión en Javascript

Vista - HTML pelado con un control texto y un botón convertir
En 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 conversiones

Creando 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 Grails

Vamos 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

  1. En lugar de usar el id (de javascript), Grails usa la propiedad name de los controles.
  2. Necesitamos un form y el botón tiene que hacer el submit (puede dejarse como button pero eso implica disparar manualmente la URL usando javascript, preferimos no hacerlo). Pero usamos el g:actionSubmit de Grails que sabe armar la URL adecuada.
  3. El div resultado va a mostrar lo que el controller le pase, primero usamos una variable resultado
<!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 Gramos

Ahora necesitamos un conversor de onzas a gramos (29.5735296875).
  • Opción 1: ponemos un combo, sería lo más razonable. En el controller tendríamos un switch feo, podríamos tener un mapa de conversores.
  • Opción 2: lo vamos a hacer didáctico.
Vamos a crear dos controllers: MillasToKms y OnzasAGramosControllers.
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 general

Aparece el modelo

Vemos lo que se repite. Además ya a esta altura parece piola abstraer un modelo: 
  • valorOrigen, 
  • factor de conversión, 
  • valor destino, 
  • y un método convertir(). 
Un detalle piola es meter la unidad de origen y de destino para mostrar en los labels.

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 abstracto

y 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:
  • si tenemos que mostrar una película, o un cliente
  • o cargar un dato con un autocompletado
  • o también podríamos armar un componente para cargar rangos de números, o fechas, etc.
  • o para reutilizar vistas que se repiten, como el Monitoreo de consultas, el inbox y la consulta de recetas.
Borramos los directorios millasAKm y onzasAGramos que tienen adentro los viejos gsp
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 Fahrenheit

Metemos el conversor Celsius a Fahrenheit: fahrenheit = (celsius * 9.0) / 5.0 + 32
Creamos 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 error

Como 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 Taglib

Armamos un taglib sencillo, que va a mostrar
  • formato decimal de n decimales, donde el n se lo pasamos en el html
  • y lo convertimos a coma
En la solapa TagLib hacemos New Taglib : ar.edu.NumericTagLib

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>

Comments