Get it on Google Play

Desarrollo Liferay 7: Agregar tipos de campos en los formularios: Sangre, sudor y lágrimas…

04-08-2017
 

Algo super importante en la creación de portales corporativos son los formularios, una potente aplicación para formularios es una navaja suiza que utilizarán todos los departamentos de la empresa para comunicarse interna(Empleados, etc.) o externamente(Clientes, partners, etc.). La herramienta tiene que ser muy personalizable ya que cada cliente requerirá de integraciones, formatos, diseños, etc.

El caso es que Liferay ha captado el mensaje y tiene una potente herramienta de formularios en Liferay 7 y DXP, aunque por el título ya habréis captado que crear tipos de campo nuevos no es tarea fácil.

Vamos a ver como podemos ponernos manos a la obra para crear un tipo de contenido nuevo como el de la foto(Portal User Full Name):

Liferay form field types list

Liferay form field types list

Configurar las dependencias de Gradle

Lo primero que tendremos que hacer es configurar las dependencias de Gradle(Fichero build.gradle):

dependencies {
	compileOnly group: "com.liferay", name: "com.liferay.dynamic.data.mapping.api", version: "3.2.0"
	compileOnly group: "com.liferay", name: "com.liferay.dynamic.data.mapping.form.field.type", version: "2.0.0"
	compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
	compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"
}

Este paso es muy sencillo.

Configurar el fichero bnd.bnd

Bundle-Name: new-form-fields
Bundle-SymbolicName: new.form.fields
Bundle-Version: 1.0.0
Liferay-JS-Config: /META-INF/resources/config.js
Web-ContextPath: /forms-new-fields	

Aquí configuraremos los atributos: ‘Liferay-JS-Config’ definiendo un fichero de configuración Javascript que mas adelante crearemos y ‘Web-ContextPath’ con el context path en el que escuchará el plugin(En este caso será /o/forms-new-fields).

Crear la classe componente que define el tipo de campo

La primera classe Java a generar configura el tipo de componente:

package com.lostsys.xxx.ddm;

import com.liferay.dynamic.data.mapping.form.field.type.BaseDDMFormFieldType;
import com.liferay.dynamic.data.mapping.form.field.type.DDMFormFieldType;

import org.osgi.service.component.annotations.Component;

/**
 * @author albert
 */
@Component(
	immediate = true,
	property = {
		"ddm.form.field.type.display.order:Integer=9",
		"ddm.form.field.type.icon=text",
		"ddm.form.field.type.js.class.name=Liferay.DDM.Field.Userfullname",
		"ddm.form.field.type.js.module=forms-new-fields",
		"ddm.form.field.type.label=Portal User Full Name",
		"ddm.form.field.type.name=userfullname"
	},
	service = DDMFormFieldType.class
)
public class UserfullnameType extends BaseDDMFormFieldType {

	@Override
	public String getName() {
		return "userfullname";
	}

}

Esta classe sobrescribe un único método que devuelve el nombre del tipo de campo, en este caso ‘userfullname’. Aunque donde esta la ‘chicha’ de verdad es en las propiedades:

  • ddm.form.field.type.display.order:Integer: Peso del tipo de campo, para que salga mas arriba o mas abajo en el listado de tipos de campos.
  • ddm.form.field.type.icon: Icono del tipo de campo, en este caso ‘text’ que viene representado por el dibujo de una ‘A’.
  • ddm.form.field.type.js.class.name: Classe Javascript que vamos a tener que declarar mas adelante.
  • ddm.form.field.type.js.module: Id del módulo que contiene el tipo de campo.
  • ddm.form.field.type.label: Etiqueta o texto descriptivo del tipo de campo.
  • ddm.form.field.type.name: Nombre o Id del tipo de campo.

Crear classe que pinta(Renderiza) el tipo de campo

Vamos a ver ahora esta clase que es la encargada de hacer el renderizado del tipo de campo:

package com.lostsys.xxx.ddm;

import com.liferay.dynamic.data.mapping.form.field.type.BaseDDMFormFieldRenderer;
import com.liferay.dynamic.data.mapping.form.field.type.DDMFormFieldRenderer;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.ServiceContextThreadLocal;
import com.liferay.portal.kernel.template.Template;
import com.liferay.portal.kernel.template.TemplateConstants;
import com.liferay.portal.kernel.template.TemplateResource;

import java.util.Map;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;

/**
 * @author albert
 */
@Component(
	immediate = true, 
	property = "ddm.form.field.type.name=userfullname",
	service = DDMFormFieldRenderer.class
)
public class UserfullnameRenderer extends BaseDDMFormFieldRenderer {

	@Override
	protected String render(Template template) throws PortalException {
		User u=ServiceContextThreadLocal.getServiceContext().getThemeDisplay().getUser();
		
		template.put("userFullname", u.getFullName() );
		template.put("userMail", u.getEmailAddress() );

		// TODO Auto-generated method stub
		return super.render(template);
		}
	
	@Override
	public String getTemplateLanguage() {
		return TemplateConstants.LANG_TYPE_FTL;
	}

	@Override
	public String getTemplateNamespace() {
		return "ddm.userfullname";
	}

	@Override
	public TemplateResource getTemplateResource() {
		return _templateResource;
	}

	@Activate
	protected void activate(Map<String, Object> properties) {
		_templateResource = getTemplateResource("/META-INF/resources/userfullname.ftl");
		}

	private TemplateResource _templateResource;

}

Esta classe tiene una sola propiedad(ddm.form.field.type.name) que enlaza con la classe del tipo de componente. Aquí se configuran un montón de cosas:

  • El método ‘render’ donde envío a la plantilla vista dos atributos que recojo del Objeto ThemeDisplay: userFullname y userMail.
  • El método ‘getTemplateLanguage’ donde configuramos el tipo de plantillas que vamos a usar, en este caso FreeMarker(FTL). He visto en alguna documentación de Liferay que usan Clojure, aunque a mi no me ha funcionado.
  • El método ‘getTemplateNamespace’ que lo tendremos que tener en cuenta cuando creemos la classe Javascript que gestionará el renderizado del tipo de campo.
  • El método ‘getTemplateResource’ que simplemente devuelve ‘_templateResource’
  • Y finalmente el método ‘activate’ donde instanciaremos la plantilla FTL que vamos a usar

Si hasta ahora esto os ha parecido sencillo, esperaos que viene lo bueno…

Creamos la plantilla de FreeMarker

Es el momento de crear la plantilla de Freemarker, en este caso ‘userfullname.ftl'(En la carpeta ‘src/main/resources/META-INF/resources’):

<div class="form-group <#if showLabel>visible<#else>hide</#if>" data-fieldname="${name}">
		<#if showLabel >
			<label class="control-label">
				${label}

				<#if required >
					<span class="icon-asterisk text-warning"></span>
				</#if>
			</label>

			<#if tip!='' >
				<p class="liferay-ddm-form-field-tip">${tip}</p>
			</#if>
		</#if>
		
		<div class="input-group-container ">
			${userFullname}
			<input class="field form-control" dir="ltr" id="${name}" name="${name}" type="hidden" value="${userFullname}" />
		</div>
</div>

Es muy importante que todo el componente esté dentro de un ‘DIV’ con la classe ‘form-group’ y el atributo ‘data-fieldname’, porque sino el Javascript de Liferay que controla todo empezará a dar errores poco descriptivos.

Hay que tener en cuenta que este solo será el renderizado inicial, después la classe Javascript que crearemos se encargará de renderizarlo nada mas se despliegue en el navegdor. Es bastante complejo.

Creamos el primero de los ficheros Javascript: config.js

Os acordáis del fichero Javascript ‘config.js’ que hemos definido en el ‘bnd.bnd’? Bién, pues ha llegado el momento de declararlo(En la carpeta ‘src/main/resources/META-INF/resources’):

;(function() {
	AUI().applyConfig(
		{
			groups: {
				'forms-new-fields-group': {
					base: MODULE_PATH + '/',
					combine: Liferay.AUI.getCombine(),
					modules: {
						'forms-new-fields': {
							condition: {
								trigger: 'liferay-ddm-form-renderer'
							},
							path: 'userfullname_field.js',
							requires: [
								'liferay-ddm-form-renderer-field'
							]
						},
						'forms-new-fields-template': {
							condition: {
								trigger: 'liferay-ddm-form-renderer'
							},
							path: 'userfullname_template.js',
							requires: [ ]
						}
					},
					root: MODULE_PATH + '/'
				}
			}
		}
	);
})();

Aquí definimos dos módulos que se llamarán con el evento ‘liferay-ddm-form-renderer’. Ademas, definimos dos ficheros Javascript mas que deberemos crear ‘userfullname_field.js’ y ‘userfullname_template.js’. Vamos ahora a crearlos.

Fichero Javascript con la configuración del componente

Este fichero Javascript contiene la configuración del componente(Campos que hay que configurar, etc.). En este ejemplo viene por defecto:

AUI.add(
	'forms-new-fields',
	function(A) {
		var NewFormFieldsField = A.Component.create(
			{
				ATTRS: {
					type: {
						value: 'userfullname'
					}
				},

				EXTENDS: Liferay.DDM.Renderer.Field,

				NAME: 'forms-new-fields',

				prototype: {
					}
			}
		);

		Liferay.namespace('DDM.Field').Userfullname = NewFormFieldsField;
	},
	'',
	{
		requires: ['liferay-ddm-form-renderer-field']
	}
);

Aquí tenéis que tener muy en cuenta toda la configuración de namespaces y classes que hemos configurado en las classes ‘ UserfullnameType’ y ‘UserfullnameRenderer’. Una sola cosa mal y un error javascript poco descriptivo.

Finalmente, el fichero Javascript con la plantilla que renderizará el tipo e campo

Ya solo falta el último fichero userfullname_template.js(En la misma carpeta que los anteriores):

if (typeof ddm == 'undefined') { var ddm = {}; }

ddm.userfullname = function(opt_data, opt_ignored) {
	var s="";

	s+='<div class="form-group " data-fieldname="' + opt_data.name + '">';
	s+=((opt_data.showLabel) ? '<label class="control-label">' + opt_data.label + ((opt_data.required) ? '<span class="icon-asterisk text-warning"></span>' : '') + '</label>':'');
	s+='<div class="input-group-container ">'+opt_data.value+'<input class="field form-control" dir="ltr" id="' + opt_data.name + '" name="' + opt_data.name + '" type="hidden" value="'+opt_data.value+'" /></div>';
	s+='</div>';	
	
	if ( $("div[data-fieldname='" + opt_data.name + "']").length>0 ) {
		s=$("div[data-fieldname='" + opt_data.name + "']").get(0).outerHTML;
		}
	
	return s;
	}

Este fichero, que por lo visto en Clojure se genera solo, implementa ‘ddm.userfullname'(Nombre configurado en la classe Java Render) . Esta función será la que realmente renderizará el tipo de campo.

En este caso, tengo una plantilla en Javascript, pero si el tipo de campo ya esta en la página devuelvo el mismo contenido. Esto se utiliza tanto cuando estamos editando el formulario desde la product bar como cuando estás viendo el formulario en la web.

Conclusiones del sistema para agregar tipos de campos

Después de haber perdido unas cuantas horas para poder crear un tipo de campo nuevo para los formularios he llegado a las siguientes conclusiones:

  • Es tremendamente poco robusto, cualquier error Javascript te deja fuera de juego.
  • Es muy poco intuitivo, la creación de tipos de campo es muy poco intuitiva para un desarrollador senior como yo. Demasiadas configuraciones, Javascript duplicado, etc.
  • No entiendo como utilizando un sistema de plantillas, se debe crear luego la classe Javascript para el renderizado.
  • Si el sistema de formularios es el Framework, porque debemos incluir en nuestro desarrollo cosas como tener que poner el DIV que encapsula. Debería ya traerlo el sistema, sobretodo porque no es opcional, todo deja de funcionar si no lo pones.

En fin, espero que os haya gustado y os sea de utilidad, porque me ha quedado un artículo muy largo y ya tengo ganas de empezar el fin de semana. Buen fin de semana a [email protected]

Interesado en formación Liferay?

 

Leave a Reply

© Albert Coronado Calzada