AJAX y Thymeleaf fragments

Utilizando Thymeleaf junto con Spring MVC, se puede optimizar la carga AJAX de determinados fragmentos de nuestra página web.

Hasta ahora, cuando queríamos actualizar diferentes partes de la página utilizando AJAX, utilizábamos el siguiente procedimiento:

1. Nos definíamos un VO para el objeto que quisiéramos refrescar. Este VO, o a veces una colección de ellos, es lo que se serializaba en la respuesta JSON, por lo que interesaba que el peso fuera lo menor posible. De modo que se utilizaban únicamente los atributos que queríamos “repintar”.

2. Creábamos un método AJAX en el Controller que devolviese la respuesta JSON.

3. Invocábamos la función AJAX desde nuestro código JavaScript.

4. Recorríamos la respuesta, creando nuevo contenido HTML “al vuelo”.

Esta aproximación tiene varios problemas:

– Si la interfaz cambia porque añadimos un nuevo atributo (por ejemplo, añadimos una columna a una tabla), hay que modificar también el VO.

– El Controller tiene que implementar funcionalidad de mapeo, del objeto al VO.

– La maquetación se realiza de manera efectiva en el código JavaScript, con todos los problemas que eso conlleva (previsualización, internacionalización, mantenibilidad en general).

Thymeleaf fragments permite evitar todos estos pasos. Por ejemplo, imaginemos que existe un combo con pedidos, y que cuando se selecciona uno de ellos hay que recargar otro combo con los artículos del pedido. Para ello se puede definir un método refreshItem():


@RequestMapping(value = "/refreshItem", method = RequestMethod.GET)
public String refreshItem(@RequestParam("order") String orderId, Model model) {
    List<Item> itemList = Item.findItemsByOrderId(orderId).getResultList();

    model.addAttribute("itemList", itemList);

    return "myView :: #item";
}

El método recibe un argumento con el ID del pedido que se acaba de seleccionar. Hasta aquí, igual que antes. La diferencia es que en vez de devolver un array de objetos JSON, se obtiene simplemente la lista de artículos y se añade al modelo como “itemList”. No importa que esta colección sea “pesada”, es decir, que tenga atributos que ahora mismo no nos interesan, porque no va a ser enviada al cliente.

Lo que sucede, en cambio, es que al devolver el string “myView :: #item”, Thymeleaf interpreta que tiene volver a evaluar el fragmento de esa plantilla definido por ese id, que en este caso sería un “<select/>”:


<select id="item" name="item">
    <option th:text="#{select_item}">Select item...</option>
    <option th:each="item : ${itemList}" th:value="${item.id}" th:text="${item.description}">My item</option>
</select>

Thymeleaf genera el HTML correspondiente a dicho “<select/>”, pero con los nuevos datos que han sido cargados en el modelo por el Controller, y lo devuelve en la respuesta AJAX. Es cierto que las etiquetas HTML suponen algo de overhead respecto a la situación original en la que se devolvía la colección de VOs, pero a cambio nos hemos quitado de un plumazo todos los problemas antes mencionados.

¿Y para incorporar el nuevo HTML a la página? Con jQuery, podemos hacerlo al mismo tiempo que la llamada AJAX, utilizando “$.load()”:


$("#item").load("refreshItem", $("#order").serialize());

¡Una sola línea de JavaScript para todo este proceso! Decididamente, utilizar esta técnica supone importantes ventajas.

Un poco de JSON-Schema

JSON-Schema (http://json-schema.org ) es un formato JSON para describir datos en JSON. Es en JSON lo que XSD a XML. Ofrece un contrato para definir los datos requeridos para una aplicación dada y la forma de interactuar con él.

Ejemplo

Para hacernos una idea veamos un ejemplo de un esquema JSON sencillo:

{

"$schema": "http://json-schema.org/draft-04/schema#",

"title": "Product",

"description": "A product from Acme’s catalog",

"type": "object",

"properties": {

"id": {

"description": "The unique identifier for a product",

"type": "integer"

},

"name": {

"description": "Name of the product",

"type": "string"

},

"price": {

"type": "number",

"minimum": 0,

"exclusiveMinimum": true

}

},

"required": ["id",
"
name",
"
price"]

}

Que validaría como válidos JSONs como este:

{

"id": 1,

"name": "A green door",

"price": 12.50,

"tags": ["home",
"
green"]

}

Y como inválido este por no tener el atributo price:

{

"id": 1,

"name": "A green door",

"tags": ["home", "green"]

}

Atributos de un esquema JSON

Podemos ver la referencia completa de la especificación JSON aquí: http://json-schema.org/latest/json-schema-core.html

{

"$schema": "http://json-schema.org/draft-04/schema#",

"title": "Product",

"description": "A product from Acme’s catalog",

"type": "object",

"properties": {

},

"required": ["id",
"
name",
"
price"]

}

Los atributos más utilizados en un esquema JON son:

  • “$schema”: Nos permite indicar la versión del Schema JSON que queremos usar: 0.4 o 0.3, SOFIA2 se apoya en la versión 0.4 ().
  • “title”: indicar un título con el que identificar el esquema.
  • “description”: Se puede utilizar este atributo para incluir una descripción de lo que va a representar el esquema JSON.
  • “type”: Para indicar el tipo que va a representar el esquema.
  • “properties”: Este atributo es un objeto con las definiciones de propiedades que definen los valores estáticos de una instancia de objeto. Es una lista no ordenada de propiedades. Los nombres de las propiedades se deben cumplir y el valor de las propiedades se definen a partir de un esquema, que debe cumplirse también.
  • “patternProperties”: Este atributo es un objeto con las definiciones de propiedades que definen los valores de una instancia de objeto. Es una lista desordenada de propiedades. Los nombres de las propiedades son patrones de expresiones regulares, las instancias de las propiedades deben cumplir con el patrón definido y el valor de la propiedad con el esquema que define esa propiedad.
  • “additionalProperties”: Permite indicar si la instancia JSON puede contener propiedades que no hayan sido definidas en el esquema. Tiene dos posibles valores (true o false), para indicar si se admite cualquier propiedad o no. Si no se añade la propiedad, se podrá incluir cualquier otra propiedad.
  • “required”: Permite indicar todas las propiedades que son obligatorias para una instancia JSON y que como mínimo debe incluir. Las propiedades se incluirán entre corchetes y separadas por el carácter “,”.
  • “$ref”: Define una URI de un esquema que contienen la completa representación para esa propiedad.

Veamos en este extracto de esquema un ejemplo para los atributos definidos

{

"$schema":"http://json-schema.org/draft-04/schema#",

"title":"SensorTemperatura Schema",

"type":"object",

"required":["SensorTemperatura"],

"properties":{

"_id":{

"type":"object",

"$ref":"#/identificador"

},

"SensorTemperatura":{

"type":"string",

"$ref":"#/datos"}

},

"additionalProperties":false,

En este ejemplo podemos ver que hay una propiedad que es obligatoria “SensorTemperatura” y que hay dos propiedades “_id” y “SensorTemperatura”, que incluyen una referencia a un elemento que es el que contiene la representación completa de esa propiedad.

"identificador":{

"title":"id",

"description":"Id insertado del SensorTemperatura",

"type":"object",

"properties":{

"$oid":{

"type":"string"

}

},

"additionalProperties":false

},

"datos":{

"title":"datos",

"description":"Info SensorTemperatura",

"type":"object",

"required":["identificador","timestamp","medida","unidad","coordenadaGps"],

"properties":{

"identificador":{

"type":"string"

},

"timestamp":{

"type":"object",

"required":["$date"],

"properties":{

"$date":{

"type":"string",

"format":"date-time"

}

},

"additionalProperties":false

},

"medida":{

"type":"number"

},

"unidad":{

"type":"string"},

"geometry":{

"$ref":"#/gps"

}

},

"additionalProperties":false

Como podemos ver tanto “identificador” como en “datos” son esquemas que definen su representación. Podemos ver también que no se admiten ningún tipo de propiedad que no sean las definidas (se ha incluido “additionalProperties”).

  • Enumerados: Los enumerados los representaremos a como una lista entre corchetes y separados entre el carácter “,”. Los enumerados siempre son de tipo “string”. Por ejemplo si queremos definir una propiedad llamada “tipo” que sólo pueda tener uno de los dos valores “latitud” o “longitud”, quedaría del siguiente modo:

“tipo”:{

“type”:”string”,

“enum”:[“latitud”,”longitud”]

}

Para instanciarlo, “tipo”: “latitud”

  • “ítems”: Define los elementos permitidos en un array, debe ser un esquema o un conjunto de esquemas.
  • “additonalItems”: Para indicar si se admiten elementos en el array, además de los definidos en el esquema.
  • “minItems”: Número mínimo de elementos que puede tener el array.
  • “maxItems”: Número máximo de elementos que puede tener el array.

En el siguiente ejemplo podemos ver cómo es el esquema para un array, “coordinates”, que debe ser de tipo numérico y que sólo puede tener dos elementos. También vemos que la propiedad “type”, es un enumerado con un único valor posible “Point”.

“geometry”:{

"type": "object",

"required":["coordinates","type"],

"properties":{

"coordinates":{

"type":"array",

"items":{

"type":"number"

},

"minItems":2,

"maxItems":2

},

"type":{

"type":"string",

"enum":["Point"]

}

},

"additionalProperties":false

}

Una instancia para este objeto sería como el siguiente

"geometry":{

"type": "Point",

"coordinates":[110.2,1233.1]

}

Podemos encontrar más información y ejemplos en el siguiente enlace: http://json-schema.org/

Un poco de JSON-Schema

JSON-Schema (http://json-schema.org ) es un formato JSON para describir datos en JSON. Es en JSON lo que XSD a XML. Ofrece un contrato para definir los datos requeridos para una aplicación dada y la forma de interactuar con él.

Ejemplo

Para hacernos una idea veamos un ejemplo de un esquema JSON sencillo:

{

“$schema”: http://json-schema.org/draft-04/schema#“,

“title”: Product“,

“description”: A product from Acme’s catalog“,

“type”: object“,

“properties”: {

“id”: {

“description”: The unique identifier for a product“,

“type”: integer

},

“name”: {

“description”: Name of the product“,

“type”: string

},

“price”: {

“type”: number“,

“minimum”: 0,

“exclusiveMinimum”: true

}

},

“required”: [“id“,
name“,
price“]

}

Que validaría como válidos JSONs como este:

{

“id”: 1,

“name”: A green door“,

“price”: 12.50,

“tags”: [“home“,
green“]

}

Y como inválido este por no tener el atributo price:

{

“id”: 1,

“name”: A green door“,

“tags”: [“home“, green“]

}

Atributos de un esquema JSON

Podemos ver la referencia completa de la especificación JSON aquí: http://json-schema.org/latest/json-schema-core.html

{

“$schema”: http://json-schema.org/draft-04/schema#“,

“title”: Product“,

“description”: A product from Acme’s catalog“,

“type”: object“,

“properties”: {

},

“required”: [“id“,
name“,
price“]

}

Los atributos más utilizados en un esquema JON son:

  • “$schema”: Nos permite indicar la versión del Schema JSON que queremos usar: 0.4 o 0.3, SOFIA2 se apoya en la versión 0.4 ().
  • “title”: indicar un título con el que identificar el esquema.
  • “description”: Se puede utilizar este atributo para incluir una descripción de lo que va a representar el esquema JSON.
  • “type”: Para indicar el tipo que va a representar el esquema.
  • “properties”: Este atributo es un objeto con las definiciones de propiedades que definen los valores estáticos de una instancia de objeto. Es una lista no ordenada de propiedades. Los nombres de las propiedades se deben cumplir y el valor de las propiedades se definen a partir de un esquema, que debe cumplirse también.
  • “patternProperties”: Este atributo es un objeto con las definiciones de propiedades que definen los valores de una instancia de objeto. Es una lista desordenada de propiedades. Los nombres de las propiedades son patrones de expresiones regulares, las instancias de las propiedades deben cumplir con el patrón definido y el valor de la propiedad con el esquema que define esa propiedad.
  • “additionalProperties”: Permite indicar si la instancia JSON puede contener propiedades que no hayan sido definidas en el esquema. Tiene dos posibles valores (true o false), para indicar si se admite cualquier propiedad o no. Si no se añade la propiedad, se podrá incluir cualquier otra propiedad.
  • “required”: Permite indicar todas las propiedades que son obligatorias para una instancia JSON y que como mínimo debe incluir. Las propiedades se incluirán entre corchetes y separadas por el carácter “,”.
  • “$ref”: Define una URI de un esquema que contienen la completa representación para esa propiedad.

Veamos en este extracto de esquema un ejemplo para los atributos definidos

{

“$schema”:”http://json-schema.org/draft-04/schema#”,

“title”:”SensorTemperatura Schema”,

“type”:”object”,

“required”:[“SensorTemperatura”],

“properties”:{

“_id”:{

“type”:”object”,

“$ref”:”#/identificador”

},

“SensorTemperatura”:{

“type”:”string”,

“$ref”:”#/datos”}

},

“additionalProperties”:false,

En este ejemplo podemos ver que hay una propiedad que es obligatoria “SensorTemperatura” y que hay dos propiedades “_id” y “SensorTemperatura”, que incluyen una referencia a un elemento que es el que contiene la representación completa de esa propiedad.

“identificador”:{

“title”:”id”,

“description”:”Id insertado del SensorTemperatura”,

“type”:”object”,

“properties”:{

“$oid”:{

“type”:”string”

}

},

“additionalProperties”:false

},

“datos”:{

“title”:”datos”,

“description”:”Info SensorTemperatura”,

“type”:”object”,

“required”:[“identificador”,”timestamp”,”medida”,”unidad”,”coordenadaGps”],

“properties”:{

“identificador”:{

“type”:”string”

},

“timestamp”:{

“type”:”object”,

“required”:[“$date”],

“properties”:{

“$date”:{

“type”:”string”,

“format”:”date-time”

}

},

“additionalProperties”:false

},

“medida”:{

“type”:”number”

},

“unidad”:{

“type”:”string”},

“geometry”:{

“$ref”:”#/gps”

}

},

“additionalProperties”:false

Como podemos ver tanto “identificador” como en “datos” son esquemas que definen su representación. Podemos ver también que no se admiten ningún tipo de propiedad que no sean las definidas (se ha incluido “additionalProperties”).

  • Enumerados: Los enumerados los representaremos a como una lista entre corchetes y separados entre el carácter “,”. Los enumerados siempre son de tipo “string”. Por ejemplo si queremos definir una propiedad llamada “tipo” que sólo pueda tener uno de los dos valores “latitud” o “longitud”, quedaría del siguiente modo:

“tipo”:{

“type”:”string”,

“enum”:[“latitud”,”longitud”]

}

Para instanciarlo, “tipo”: “latitud”

  • “ítems”: Define los elementos permitidos en un array, debe ser un esquema o un conjunto de esquemas.
  • “additonalItems”: Para indicar si se admiten elementos en el array, además de los definidos en el esquema.
  • “minItems”: Número mínimo de elementos que puede tener el array.
  • “maxItems”: Número máximo de elementos que puede tener el array.

En el siguiente ejemplo podemos ver cómo es el esquema para un array, “coordinates”, que debe ser de tipo numérico y que sólo puede tener dos elementos. También vemos que la propiedad “type”, es un enumerado con un único valor posible “Point”.

“geometry”:{

“type”: “object”,

“required”:[“coordinates”,”type”],

“properties”:{

“coordinates”:{

“type”:”array”,

“items”:{

“type”:”number”

},

“minItems”:2,

“maxItems”:2

},

“type”:{

“type”:”string”,

“enum”:[“Point”]

}

},

“additionalProperties”:false

}

Una instancia para este objeto sería como el siguiente

“geometry”:{

“type”: “Point”,

“coordinates”:[110.2,1233.1]

}

Podemos encontrar más información y ejemplos en el siguiente enlace: http://json-schema.org/