Fields Plugin

fields

A Deep Dive

Søren Berg Glasius

whoami

Søren Berg Glasius

  • Nerd By Nature

  • Java developer since y2k

  • Groovy & Grails developer since y2k+9

  • GR8Conf co-founder and organizer

  • Groovy & Grails advocate

  • Works for Gennemtænkt IT (Danish company)

GR8Conf EU 2018!

Your next Groovy fix!

  • 10th consecutive year!

  • May 30th - June 1st 2018

    • Visit Copenhagen in June!

  • Three conference days

    • with a sprinkle of DevOps

  • Early Bird Registration is open

Celebrate with us!

Session Agenda (1/2)

  • What is the Fields Plugin

  • Why use it?

  • Look at code

Session Agenda (2/2)

  • Look at more code

  • Internationalization

  • Q/A (Not really - just ask!)

Slides

Available here:

What is the Fields Plugin

The Fields plugin allows you to customize the rendering of input fields for properties of domain objects, command beans and POGOs based on their type, name, etc.

The plugin aims (1/2)

  • Good defaults for fields.

  • Override field rendering for particular properties or property types without having to replace entire form templates.

  • No copy and paste markup for containers, labels and error messages

The plugin aims (2/2)

  • Support inputs for property paths of arbitrary depth and with indexing.

  • Easy internationalization (i18n)

  • Plugins can provide field rendering for special property types that gets picked up automatically

Open source

Acknowledgements!

Fields Plugin was created by

Robert Fletcher

Large contributions by

  • Martín Caballero

  • Peter Ledbrook

  • Graeme Rocher

  • Sudhir Nimavat

Why use it?

  • It is what Grails uses for scaffolding

  • Enables customization of scaffold views

  • It helps keeping your .gsp files DRY

  • Gives you a common place to define input widgets and value display

Setting the stage (1/3)

To have a context, here are three domain classes:

Speaker.groovy
class Speaker {
    String name
    String company
    String twitter
    String bio
    Date dateOfBirth
    static hasMany = [talks: Talk]
    static constraints = {
        name nullable: false
        bio nullable: false, widget: 'textarea'
        twitter nullable: false
        company nullable: false
        talks minSize: 0
    }
}

Settings the stage (2/3)

Talk.groovy
class Talk {
    String title
    String summary

    static belongsTo = [speaker: Speaker]
    static hasMany = [tags: Tag]
    static constraints = {
        title nullable: false
        summary nullable: false, widget: 'textarea'
        speaker()
        tags()
    }
}

Settings the stage (3/3)

Tag.groovy
class Tag {
    String tag

    static hasMany = [talks: Talk]
    static belongsTo = Talk
    static constraints = {
        tag nullable: false
        talks nullable: true
    }
}

Before the Fields Plugin (1/2)

create.gsp (simplified)
<html>
<head>
    <meta name="layout" content="main"/>
</head>
<body>
<div id="create-speaker" class="content scaffold-create" role="main">
    <g:form action="save">
        <tmpl:form speaker="${speaker}"/>
        %{--<g:render template="form" model="[speaker: speaker]"/> --}%
        <fieldset class="buttons">
            <g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}"/>
        </fieldset>
    </g:form>
</div>
</body>
</html>

Before the Fields Plugin (2/2)

_form.gsp
<%@ page import="fields.demo.Speaker" %>
<div class='fieldcontain required'>
    <label for='name'><g:message code="speaker.name.label" default="Name"/>
        <span class='required-indicator'>*</span>
    </label><g:textField name="name" value="${speaker.name}" required=""/>
</div>

<div class='fieldcontain required'>
    <label for='dateOfBirth'><g:message code="speaker.dateOfBirth.label" default="Date of Birth"/>
        <span class='required-indicator'>*</span>
    </label><g:datePicker name="dateOfBirth" date="${speaker.dateOfBirth}" required="" precision="day"/>
</div>

<div class='fieldcontain'>
    <label for='bio'><g:message code="speaker.bio.label" default="Bio"/></label>
    <g:textArea name="bio" value="${speaker.bio}"/>
</div>


<div class='fieldcontain required'>
    <label for='twitter'><g:message code="speaker.twitter.label" default="Twitter"/>
        <span class='required-indicator'>*</span>
    </label><g:field type="number" name="twitter" value="${speaker.twitter}" required="" min="0"/>
</div>

<div class='fieldcontain'>
    <label for='talks'><g:message code="speaker.talks.label" default="Talks"/></label>
    <g:select id="talks" name="talks.id" from="${Talks.list()}" value="${speaker.talks}"/>
</div>

Before the Fields Plugin

Lots of LOC

With The Fields Plugin

Stay DRY

Tags in the Fields TagLib

Tags (1/2)

  • <f:widget/>

  • <f:field/>

  • <f:with>

  • <f:all>

Tags (2/2)

  • <f:displayWidget/>

  • <f:display/>

  • <f:table>

Terminology

  • Widgets

  • Wrappers

Terminology (examples 1/2)

<g:textField/>
<div class='fieldcontain required'>
    <label for='name'>
        <g:message code="speaker.name.label" default="Name"/>
        <span class='required-indicator'>*</span>
    </label>
    <!-- widget -->
    <g:textField name="name" value="${speaker.name}" required=""/>
</div>
<g:datePicker/>
<div class='fieldcontain required'>
    <label for='dateOfBirth'>
        <g:message code="speaker.dateOfBirth.label" default="Date of Birth"/>
        <span class='required-indicator'>*</span>
    </label>
    <!-- widget -->
    <g:datePicker name="dateOfBirth" date="${speaker.dateOfBirth}"
                  required="" precision="day"/>
</div>

Terminology (examples 2/2)

<g:textArea/>
<div class='fieldcontain'>
    <label for='bio'>
        <g:message code="speaker.bio.label" default="Bio"/>
    </label>
    <!-- widget -->
    <g:textArea name="notes" value="${speaker.bio}"/>
</div>
<g:select/>
<div class='fieldcontain'>
    <label for='speaker'>
        <g:message code="talk.speaker.label" default="Speaker"/>
    </label>
    <!-- widget -->
    <g:select name="speaker.id"
              from="${Speaker.list()}"
              value="${talk.speaker}"/>
</div>

Dig into code…​

Customizing rendering

  • Template lookup

  • Template weaving

  • Parameters in templates

  • Wrapper and widget selection

Template lookup (1/2)

  • From fine-grained to generic

  • Based on:

    • Controller name

    • Action name

    • Bean type

    • Bean name

    • Property type

    • Property name

Template lookup (2/2)

  • grails-app/views/controllerName/actionName/propertyName/

  • grails-app/views/controllerName/actionName/propertyType/

  • grails-app/views/controllerName/actionName/

  • grails-app/views/controllerName/propertyName/

  • grails-app/views/controllerName/propertyType/

  • grails-app/views/controllerName/

  • grails-app/views/_fields/class/propertyName/

  • grails-app/views/_fields/superclass/propertyName/

  • grails-app/views/_fields/associationType/

  • grails-app/views/_fields/propertyType/

  • grails-app/views/_fields/propertySuperclass/

  • grails-app/views/_fields/default/

Template lookup example (1/2):

<f:field bean="speaker" property="name"/>
url: http://localhost:8080/speaker/create
  • grails-app/views/speaker/create/name/_wrapper.gsp

  • grails-app/views/speaker/create/string/_wrapper.gsp

  • grails-app/views/speaker/create/_wrapper.gsp

  • grails-app/views/speaker/name/_wrapper.gsp

  • grails-app/views/speaker/string/_wrapper.gsp

  • grails-app/views/speaker/_wrapper.gsp

  • grails-app/views/_fields/speaker/name/_wrapper.gsp

  • grails-app/views/_fields/string/_wrapper.gsp

  • grails-app/views/_fields/default/_wrapper.gsp

Template lookup example (2/2):

<f:field bean="speaker" property="name"/>
url: http://localhost:8080/speaker/create
  • grails-app/views/speaker/create/name/_widget.gsp

  • grails-app/views/speaker/create/string/_widget.gsp

  • grails-app/views/speaker/create/_widget.gsp

  • grails-app/views/speaker/name/_widget.gsp

  • grails-app/views/speaker/string/_widget.gsp

  • grails-app/views/speaker/_widget.gsp

  • grails-app/views/_fields/speaker/name/_widget.gsp

  • grails-app/views/_fields/string/_widget.gsp

  • grails-app/views/_fields/default/_widget.gsp

Parameters in templates (1/5)

For <f:field/>, <f:widget/>, <f:display/> and <f:displayWidget/>

NameTypeDescription

bean

Object

Bean object for value

property

String

Name of the property in question

value

Object

The current value of the property in the bean

Parameters in templates (2/5)

NameTypeDescription

label

String

Label for UI

type

Class

Type of the property (eg. String or Integer).

required

boolean

true if the field is required (based on nullable and/or blank constraints).

Parameters in templates (3/5)

NameTypeDescription

constraints

ConstrainedProperty

Domain or command constraints

persistentProperty

PersistentProperty or GrailsDomainClassProperty

Persistent property object (only Domain objects).

Parameters in templates (4/5)

NameTypeDescription

invalid

boolean

true if a property has field errors.

errors

List<String>

The list of field errors.

prefix

String

Prefix for input name like name="${prefix}propertyName". The label is also modified.

Parameters in templates (5/5)

For <f:field/> and <f:display/> only:

NameTypeDescription

widget

String

<f:widget/> or body() output

I18N

Why the name?

"Internationalization"[1..-2].size() == 18

I18N Convention (1/2)

Translation keys based on bean name and property name

Speaker.groovy
package fields.demo

class Speaker {
    String name
    String company
    String twitter
    String bio
    String image

    Date dateCreated
    Date lastUpdated
}
getCode() { return "${beanName.unCapitalize()}.${property name}.label" }
getDefault() { return "${GrailsNameUtils.getNaturalName(propertyName)" }

Example: Date dateOfBirth becomes Date Of Birth

I18N Convention (2/2)

Translation for dateOfBirth:

messages.properties
speaker.dateOfBirth.label=Date of Birth
messages_es.properties
speaker.dateOfBirth.label=Fecha de nacimiento
messages_da.properties
speaker.dateOfBirth.label=Fødselsdag

Summary

The Fields Plugin can help to

  • Maintain look and feel throughout app

  • Centralize and organize widgets and wrappers

  • Make consistent i18n translations

  • Keep .gsp-files DRY

  • Share look and feel between apps

Questions

and answers?

Thank You.

Thank You.

'