Introduction

Goals

With dueuno:elements you can develop backoffice web applications writing code in a single programming language: Apache Groovy. No need to know HTML, CSS o Javascript.

With dueuno:elements you can develop and maintain web applications with requirements such as:

  1. Authenticate & authorize users (who can access and what they can do)

  2. Implement several business features with a coherent user interface

  3. Display and edit data in table format (CRUD)

  4. Display charts, dashboards, etc. to build Business Intelligence applications

  5. Let users customize their localization preferences with country specific formats (languages, dates, numbers, currencies, quantities, etc.)

  6. Develop multi-device applications. Each application will automatically work on a desktop pc with a mouse as well as on a mobile device with a finger.

  7. Develop multi-tenant applications. The same application can serve different clients with separate databases.

The main purpose of dueuno:elements is to decouple the business logic from the User Interface (GUI). This lowers the costs of maintaining the application: just one language instead of 5 (HTML, CSS, JavaScript, Java, SQL), less skilled people can join the team and no need to upgrade the GUI when new web technologies/standards gets available.

Non-Goals

dueuno:elements is NOT a solution for:

  1. Creating classical websites (text + images + basic user interaction)

  2. Creating graphical/animated web applications

  3. Developing applications where the client retains strict control on how the user interface will look like

For such cases you can just use the beautiful Grails.

Quick Start

Basics

To develop dueuno:elements applications you need to learn the basics of the Apache Groovy programming language and the basics of the Grails framework.

Learning Groovy

The Groovy programming language has been around for more than 20 years now, it’s been the second language to be developed for the JVM. You can read the documentation and access the Groovy learning resources

Learning Grails

Grails has been helping web developers for 20 years now, it still is a growing and evolving technology so what you want to do is reading the latest documentation on the Grails website

Run

We are going to create our first dueuno:elements application, ready?

  1. Download and install IntelliJ IDEA Community Edition.

  2. Download the Application Template

    1. Unzip it into your home directory

    2. Open it with IntelliJ IDEA

    3. Run the application from the Gradle sidebar clicking on project-name → Tasks → application → bootRun

| Running application…​

Configuring Spring Security Core …​
…​ finished configuring Spring Security Core

Grails application running at https://localhost:8080 in environment: development
The first run will take some time since it has to download all the needed dependencies.

We can now navigate to https://localhost:8080 to see the login screen. Login with the credentials super/super to play around with the basic features of a plain _dueuno:elements application.

Application Login

Basics

In dueuno:elements everything is a component. All visual objects of the application are derived from the same base class Component and they can be assembled together like we do with a LEGO set.

Some of these components are automatically created and managed by dueuno:elements for each application instance. Let’s give them a quick look.

The Shell

We call Shell the dueuno:elements GUI. Each dueuno:elements application share a common user experience and content structure.

Login

Where you can log in.

Login

Home

Where you can find your favourite Features

Groups

Application Menu

Where you can find the complete Features list.

Application Menu

User Menu

Where you can find the user options.

User Menu

Navigation Bar

The user can access (from left to right) (1) the Main Menu, (2) the Home - where users can find their favourite features - and (3) the User Menu.

Navigation Bar

Content

Each Feature will display as an interactive screen we call Content that will occupy the main area surrounded by the Shell.

Content

Contents can be displayed as modals. A modal content is rendered inside a dialog window. This lets the user focus on a specific part of the Feature to accomplish subtasks like editing a specific object.

Modals can be displayed in three sizes: normal (default), wide and fullscreen.

Modal Content

User Messages

The application can display messages to the user to send alerts or confirm actions.

Message

Responsiveness

All dueuno:elements applications work both on desktop computers and on mobile devices by design and without the developer having to cope with it. Here is how an application looks like on a Desktop, on a Tablet and on a Mobile Phone.

Desktop
Tablet
Phone

Project Structure

dueuno:elements applications are Grails applications. The project structure, follows the conventions over configuration design paradigm so each folder contains specific source file types.

Filesystem

/myapp                      (1)

  /grails-app               (2)
    /controllers            (3)
    /services               (4)
    /domain                 (5)
    /i18n                   (6)
    /init                   (7)
    /conf                   (8)

  /src
    /main
      /groovy              (9)
1 Project root
2 Web Application root
3 User Interface. Each class name under this directory must end with Controller (Eg. PersonController)
4 Business Logic. Each class name under this directory must end with Service (Eg. PersonService)
5 Database. Each class name under this directory must begin with T (Eg. TPerson)
6 Translations
7 Initialization
8 Configuration files
9 Other application source files

Each folder contains the package structure, so for example if your application main package is myapp the source file structure will look like this:

/myapp
  /grails-app

    /controllers
      /myapp
        MyController.groovy

    /services
      /myapp
        MyService.groovy

    /domain
      /myapp
        MyDomainClass.groovy

    /init
      /myapp
        BootStrap.groovy

  /src
    /main
      /groovy
        /myapp
          MyClass.groovy

Features

A dueuno:elements application is a set of Features.

Each Feature consists of a set of visual objects the user can interact with to accomplish specific tasks. You can identify each Feature as an item in the application menu on the left. Clicking a menu item will display the content of the selected Feature.

To configure the application Features we register them in the BootStrap.groovy file (See registerFeature()):

/grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService (1)

    def init = { servletContext ->

        applicationService.init { (2)
            registerFeature( (3)
                    controller: 'person',
                    icon: 'fa-user',
            )
        }

    }
}
1 ApplicationService is the object in charge of the application setup
2 The init = { servletContext → …​ } Grails closure is executed each time the application starts up
3 Within the applicationService.init { …​ } closure you can call any of the applicationService methods. In this case the method registerFeature()

Controllers

A Feature links to a controller.

A controller is a set of actions.

All actions that a user can take on the application (eg. a click on a button) are coded as methods of a controller. Each action corresponds to a URL that will be submitted from the browser to the server. The URL follows this structure:

http://my.company.com/${controllerName}/${actionName}

For example the following Controller contains two actions that can be called like this:

http://my.company.com/person/index (1)
http://my.company.com/person/edit/1 (2)
/grails-app/controllers/PersonController.groovy
class PersonController implements ElementsController {

    def index() { (1)
        dispaly ...
    }

    def edit() {  (2)
        display ... (3)
    }

}
1 The index action. It’s the default one, it can also be called omitting the action name, eg. http://my.company.com/person
2 The edit action
3 The display method ends each action and tells the browser what component to display

Services

We don’t implement business logic in Controllers. We do it in Services. Each Service is a class implementing several methods we can call from a Controller.

For example the following Service implements the method sayHello().

/grails-app/services/PersonService.groovy
class PersonService {

    String sayHello() {
        return "Hi folks!"
    }

}

We can call it from a Controller like this:

/grails-app/controllers/PersonController.groovy
class PersonController implements ElementsController {

    PersonService personService (1)

    def index() {
        def hello = personService.sayHello()
        display message: hello (2)
    }

}
1 Service injection, the variable name must be the camelCase version of the PascalCase class name
2 the display method renders objects on the browser, in this case a message

Database

Each application has a DEFAULT database connection defined in the grails-app/conf/application.yml file. This DEFAULT connection cannot be changed at runtime and it is used by dueuno:elements to store its own database.

  1. You can configure multiple databases per environment (DEV, TEST, PRODUCTION, ect) in the application.yml, see: https://docs.grails.org/latest/guide/single.html#environments

  2. You can edit/create database connections at runtime from the dueuno:elements GUI accessing with the super user from the menu System Configuration → Connection Sources

  3. You can programmatically create database connections at runtime with the ConnectionSourceService as follows:

/grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService (1)
    ConnectionSourceService connectionSourceService (2)

    def init = { servletContext ->

        applicationService.onInstall { String tenantId -> (3)
            connectionSourceService.create( (4)
                    name: 'runtimeDatasource',
                    driverClassName: 'org.h2.Driver',
                    dbCreate: 'update',
                    username: 'sa',
                    password: '',
                    url: 'jdbc:h2:mem:DYNAMIC_CONNECTION;LOCK_TIMEOUT=10000 DB_CLOSE_ON_EXIT=TRUE',
            )
        }
    }

}
1 ApplicationService is the object in charge of the application setup
2 ConnectionSourceService service injection
3 The onInstall { …​ } closure is called only the first time the application runs for the DEFAULT Tenant and each time a new Tenant is created
4 The create() method creates a new connection and connects to it. Once created the application will automatically connect to it each time it boots up. Connection details can be changed via GUI accessing as super from the menu System Configuration → Connection Sources

Tenants

Multi-Tenants applications share the code while connecting to different databases, usually one for each different company. This way data is kept separated with no risk of disclosing data from one company to the other.

Each application user can only belong to one Tenant. If a person needs to access different Tenants two different accounts must be created. To configure and manage users for a Tenant you have to access the application as the admin user. For each Tenant a default admin user is created with the same name as the Tenant (E.g. the Tenant called 'TEST' is going to have a 'test' user which is the Tenant administrator.

The default password for such users corresponds to their names. To change the password you need to log in with the admin user and change it from the User Profile. Go to User Menu (top right) → Profile.

New Tenants can be created from the dueuno:elements GUI accessing as super from the menu System Configuration → Tenants. If multi-tenancy is not a requirement to your application you will be using the DEFAULT Tenant which is automatically created.

User Management

Users can access a dueuno:elements application with credentials made of a username and a secret password. Each user must be configured by the Tenant’s admin user from the menu System Administration → Users and System Administration → Groups.

Groups

Building Applications

In this chapter we are going through the building of a dueuno:elements application.

CRUD

One of the most useful GUI pattern is the CRUD (Create, Read, Update, and Delete). It is based on the four basic operations available to work with persistent data and databases.

Applications are made of features, we register one to work with movies (See Features).

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService (1)

    def init = { servletContext ->
        applicationService.init {
            registerFeature( (2)
                    controller: 'movie',
                    icon: 'fa-film',
                    favourite: true,
            )
        }
    }
}

We are going to implement a simple database with GORM for Hibernate on top of which we can build our GUI.

grails-app/domain/TMovie.groovy
class TMovie implements MultiTenant<TMovie> {
    LocalDateTime dateCreated

    String title
    Integer released

    static hasMany = [actors: TActor]

    static constraints = {
    }
}
grails-app/domain/TActor.groovy
class TActor implements MultiTenant<TActor> {
    LocalDateTime dateCreated

    String firstname
    String lastname

    static constraints = {
    }
}

To create a CRUD user interface we are going to implement a controller with the following actions. The business logic will be implemented into a service to keep it decoupled from the GUI.

grails-app/controllers/BookController.groovy
@Secured(['ROLE_CAN_EDIT_MOVIES']) (1)
class MovieController implements ElementsController { (2)

    def index() {
        // will display a list of movies
    }

    def create() { (3)
        // will display a form with the movie title
    }

    def onCreate() { (3)
        // will create the movie record on the database
    }

    def edit() {
        // will display the details of a movie
    }

    def onEdit() {
        // will update the movie record on the database
    }

    def onDelete() {
        // will delete a movie record from the database
    }
}
1 Only users with the ROLE_CAN_EDIT_MOVIES authority can access the actions in this controller.
2 Implementing ElementsController the dueuno:elements API will become available
3 As a convention, all actions building and displaying a GUI are named after a verb or a name while all actions that execute a business logic are identified by a name starting with on.

We are going to use the ContentList content to list the records, the ContentCreate and ContentEdit contents to create a new record and edit an existing one (See Contents).

grails-app/controllers/BookController.groovy
@Secured(['ROLE_CAN_EDIT_MOVIES'])
class MovieController implements ElementsController {

    MovieService movieService (1)

    def index() {
        def c = createContent(ContentList)
        c.table.with {
            filters.with {
                fold = false
                addField(
                        class: TextField,
                        id: 'find',
                        label: 'default.filters.text',
                        cols: 12,
                )
            }
            sortable = [
                    title: 'asc',
            ]
            columns = [
                    'title',
                    'released',
            ]

            body.eachRow { TableRow row, Map values ->
                // Do not execute slow operations here to avoid slowing down the table rendering
            }
        }

        def filters = c.table.filters.values
        c.table.body = movieService.list(filters, params)
        c.table.paginate = movieService.count(filters)

        display content: c
    }

    private buildForm(TMovie obj = null) {
        def c = obj
                ? createContent(ContentEdit)
                : createContent(ContentCreate)

        c.form.with {
            validate = TMovie
            addField(
                    class: TextField,
                    id: 'title',
            )
            addField(
                    class: NumberField,
                    id: 'released',
            )
        }

        if (obj) {
            c.form.values = obj
        }

        return c
    }

    def create() {
        def c = buildForm()
        display content: c, modal: true
    }

    def onCreate() {
        def obj = movieService.create(params)
        if (obj.hasErrors()) {
            display errors: obj
            return
        }

        display action: 'index'
    }

    def edit() {
        def obj = movieService.get(params.id)
        def c = buildForm(obj)
        display content: c, modal: true
    }

    def onEdit() {
        def obj = movieService.update(params)
        if (obj.hasErrors()) {
            display errors: obj
            return
        }

        display action: 'index'
    }

    def onDelete() {
        try {
            movieService.delete(params.id)
            display action: 'index'

        } catch (e) {
            display exception: e
        }
    }
}
1 Service injection, see the implementation below

We will implement the database operations using GORM for Hibernate, the default Object Relational Mapper used by Grails.

grails-app/services/MovieService.groovy
@CurrentTenant
class MovieService {

    private DetachedCriteria<TMovie> buildQuery(Map filters) {
        def query = TMovie.where {}

        if (filters.containsKey('id')) query = query.where { id == filters.id }

        if (filters.find) {
            String search = filters.find.replaceAll('\\*', '%')
            query = query.where {
                title =~ "%${search}%"
            }
        }

        // Add additional filters here

        return query
    }

    TMovie get(Serializable id) {
        // Add any relationships here (Eg. references to other DomainObjects or hasMany)
        Map fetch = [
                actors: 'join',
        ]

        return buildQuery(id: id).get(fetch: fetch)
    }

    List<TMovie> list(Map filters = [:], Map params = [:]) {
        if (!params.sort) params.sort = [dateCreated: 'asc']

        // Add single-sided relationships here (Eg. references to other Domain Objects)
        // DO NOT add hasMany relationships, you are going to have troubles with pagination
//        params.fetch = [
//                actors: 'join',
//        ]

        def query = buildQuery(filters)
        return query.list(params)
    }

    Integer count(Map filters = [:]) {
        def query = buildQuery(filters)
        return query.count()
    }

    TMovie create(Map args = [:]) {
        if (args.failOnError == null) args.failOnError = false

        TMovie obj = new TMovie(args)
        obj.save(flush: true, failOnError: args.failOnError)
        return obj
    }

    TMovie update(Map args = [:]) {
        Serializable id = ArgsException.requireArgument(args, 'id')
        if (args.failOnError == null) args.failOnError = false

        TMovie obj = get(id)
        obj.properties = args
        obj.save(flush: true, failOnError: args.failOnError)
        return obj
    }

    void delete(Serializable id) {
        TMovie obj = get(id)
        obj.delete(flush: true, failOnError: true)
    }
}

Run the application with gradle bootRun, you should be able to create, list, edit and delete movies.

Custom Contents

In the context of a controller action we can display to the user any content we need. To create a custom content we suggest creating a ContentBlank (See Contents) adding the components wes need (See Components).

Building Components

In this chapter we are going through the building of a dueuno:elements component.

Pages

TBD

Contents

TBD

Components

TBD

Controls

TBD

API Reference

Dueuno Elements is a plugin for the Grails Framework that we can use to develop backoffice applications.

Quick Start

Create an application

Use Grails Forge to generate a Web Application with Java 17 (we support only Java 17) then add the following repository and dependency to your newly created Grails application:

build.gradle
repositories {
    …​
    maven { url "http://repo.dueuno.com:8888/repository/snapshot"; allowInsecureProtocol = true }
}

dependencies {
    …​
    implementation "com.dueuno:elements:24-SNAPSHOT"
}

Add the following code to initialize the application:

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->
        applicationService.init {
            // no-op (1)
        }
    }
}
1 It’s okay to leave the init closure empty, but the declaration must exist otherwise the application will not be initialized and you won’t be able to login.
Just delete the autogenerated file grails-app/controllers/**/UrlMappings.groovy if you want to present a login screen to your users when they first reach your application.

Optional steps

Configure the application to display the dueuno:elements logs and banner. Add the following code to the relative files:

grails-app/conf/application.yml
spring:
  main:
    banner-mode: "log"
grails-app/conf/logback.xml
<logger name="org.springframework.boot.SpringApplication" level="INFO" />
<logger name="dueuno" level="INFO" />

Login

At this point you should be able to run the application, point your browser to http://localhost:8080 and login with the system administrator credentials (username: super, password: super).

Application

ApplicationService is the main component in charge of the application setup, mainly used in BootStrap.groovy. The following are the methods it exposes.

init()

Initializes the application. This closure gets called each time the application is executed.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService (1)

    def init = { servletContext ->

        applicationService.init { (2)
            // ...
        }

    }
}
1 Injects an instance of the ApplicationService
2 The init { …​ } closure is executed each time the application starts up

beforeInit()

Gets executed before the application is initialized. The session is not available you can NOT set session variables from here.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.beforeInit {
            // ...
        }

    }
}

afterInit()

Gets executed after the application is initialized. The session is not available you can NOT set session variables from here.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.afterInit {
            // ...
        }

    }
}

afterLogin()

Gets executed after the user logged in. The session is active, you can set session variables from here.

grails-app/init/BootStrap.groovy
class BootStrap {

    SecurityService securityService (1)

    def init = { servletContext ->

        securityService.afterLogin { GrailsHttpSession session ->
            // ...
        }

    }
}
1 Injects an instance of the SecurityService

afterLogout()

Gets executed after the user logged in. The session is NOT active, you can NOT manage session variables from here.

grails-app/init/BootStrap.groovy
class BootStrap {

    SecurityService securityService (1)

    def init = { servletContext ->

        securityService.afterLogout {
            // ...
        }

    }
}
1 Injects an instance of the SecurityService

onInstall()

Installs the application. This closure gets called only once when the application is run for the first time. It is executed for the DEFAULT tenant and when a new tenant is created from the super admin GUI.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.onInstall { String tenantId -> (1)
            // ...
        }

    }
}
1 The tenantId tells what tenant is being installed

onSystemInstall()

Gets executed only the first time the application is run.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.onSystemInstall {
            // ...
        }

    }
}

onPluginInstall()

Gets executed only the first time the application is run. It is used to install plugins.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.onPluginInstall { String tenantId ->
            // ...
        }

    }
}

onDevInstall()

Gets executed only once if the application is run from the IDE (only when the development environment is active). You can use this to preload data to test the application.

This closure will NOT be executed when the application is run as JAR, WAR or when the test environment is active.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.onDevInstall { String tenantId ->
            // ...
        }

    }
}

onUpdate()

On application releases, may you need to update the database or any other component, you can programmatically do it adding an onUpdate closure.

These closures get executed only once when the application starts up. The execution order is defined by the argument, in alphabetical order.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext -> (1)

        applicationService.onUpdate('2021-01-03') { String tenantId ->
            println "${tenantId}: UPDATE N.2"
        }

        applicationService.onUpdate('2021-01-02') { String tenantId ->
            println "${tenantId}: UPDATE N.1"
        }

        applicationService.onUpdate('2021-01-05') { String tenantId ->
            println "${tenantId}: UPDATE N.4"
        }

        applicationService.onUpdate('2021-01-04') { String tenantId ->
            println "${tenantId}: UPDATE N.3"
        }
    }
}
1 The closures will be executed in the following order based on the specified version string: 2021-01-02, 2021-01-03, 2021-01-04, 2021-01-05.

registerPrettyPrinter()

Registers a string template to render an instance of a specific Class. A pretty printer can be registered with just a name, in this case it must be explicitly assigned to a Control when defining it.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.init {
            registerPrettyPrinter(TProject, '${it.name}') (1)
            registerPrettyPrinter('PROJECT_ID', '${it.padLeft(4, "0")}') (2)
        }

    }
}
1 Registers a pretty printer for the TProject domain class. The it variable will refer to an instance of a TProject in this case we will display the name property
2 Registers a pretty printer called PROJECT_ID. Since we know that the project id is going to be a String we can call the padLeft() method on it

registerTransformer()

Registers a callback used to render an instance of a specific Class. To make it work it must be explicitly assigned to a Control when defining it.

The closure will receive the value that is being transformed and must return a String.
Be careful when using transformers since it may impact performances when the closure takes long time to execute.
grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService
    SecurityService securityService

    def init = { servletContext ->

        applicationService.init {
            registerTransformer('USER_FULLNAME') { Object value ->
                return securityService.getUserByUsername(value).fullname
            }
        }

    }
}

registerCredits()

Registers a role along with the people who took that role during the development of the project. When a credit reference is registered a new menu item will appear in the User Menu.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.init {
            registerCredits('Application Development', 'Francesco Piceghello', 'Gianluca Sartori')
        }

    }
}

Features

A dueuno:elements application is a container for a finite set of features that you want to expose to the users. Features are defined in the init closure. The main menu on the right side of the GUI lists all the features accessible by a user depending on its privileges.

Once defined, features are than implemented in Controllers & Actions.

registerFeature()

Registers a Feature.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.init {
            registerFeature(
                    controller: 'book', (1)
                    action: 'index', (2)
                    icon: 'fa-book', (3)
                    authorities: ['ROLE_CAN_EDIT_BOOKS'] (4)
            )
            registerFeature(
                    controller: 'read',
                    icon: 'fa-glasses',
            )

            registerFeature(
                    controller: 'configuration', (5)
            )
            registerFeature(
                    parent: 'configuration', (6)
                    controller: 'authors',
                    icon: 'fa-user',
            )
            registerFeature(
                    parent: 'configuration',
                    controller: 'publishers',
                    icon: 'fa-user-shield',
            )
        }

    }
}
1 Name of the controller that implements the feature
2 Name of the action to execute when the feature is clicked (default: index)
3 Menu item icon, you can choose one from Font Awesome
4 The feature will be displayed only to the users configured with the roles in the list (default: ROLE_USER)
5 A feature with just a controller can be created to group features. This will become the parent feature.
6 Tells the feature which one is its parent
The controller class must be annotated with @Secured(['ROLE_CAN_EDIT_BOOKS']) to actually block all users without that authority from accessing the feature. See: Controllers & Actions

Available options:

Name Type Description

controller

String

The name of the controller that implements the feature

action

String

(OPTIONAL) The name of the action to execute (default: index)

params

Map<String, Object>

(OPTIONAL) Parameters to add when calling the action or url

submit

List<String>

(OPTIONAL) List of the component names that will be processed to retrieve the values to be passed when calling the action or url

icon

String

(OPTIONAL) Menu item icon, you can choose one from Font Awesome

authorities

List<String>

(OPTIONAL) The feature will be displayed only to the users configured with the roles in the list (default: ROLE_USER)

favourite

Boolean

(OPTIONAL) If true the feature will be displayed on the bookmark page as well (accessible clicking the home menu)

url

String

(OPTIONAL) An absolute URL. When specified it takes precedence so controller and action won’t be taken into account

direct

Boolean

(OPTIONAL) Menu items are URLs managed by dueuno:elements. When set to true the URL gets managed directly by the browser without any processing

target

String

(OPTIONAL) The feature will be displayed in a new browser tab with the provided name

targetNew

String

(OPTIONAL) The feature will be displayed in a new browser tab (_blank)

confirmMessage

String

(OPTIONAL) Message to display before the feature is displayed giving the option to cancel or confirm the operation

infoMessage

String

(OPTIONAL) If set, the message will be displayed instead of the feature

registerUserFeature()

Registers a Feature in the User Menu. For the available options see: registerFeature()

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->

        applicationService.init {
            registerUserFeature(
                    controller: 'manual',
                    icon: 'fa-book',
                    targetNew: true,
            )
        }

    }
}

Controllers & Actions

Controllers

A controller is a container for a set of actions. When a user interacts with the GUI an Action could be called to execute some logic. Actions are grouped in controllers so we can split and organize the application to fit the business domain.

A Controller is a Groovy class and each method is an Action. In the following example we see the structure of a dueuno:elements controller for a CRUD operation.

grails-app/controllers/BookController.groovy
@Secured(['ROLE_CAN_EDIT_BOOKS']) (1)
class BookController implements ElementsController { (2)

    def index() {
        // will display a list of books
    }

    def create() { (3)
        // will display a form with book title and author
    }

    def onCreate() { (3)
        // will create the book record on the database
    }

    def edit() {
        // will display the details of a book
    }

    def onEdit() {
        // will update the book record on the database
    }

    def onDelete() {
        // will delete a book from the database
    }
}
1 The @Secured annotation let all the actions from this controller be accessed only by users with the ROLE_CAN_EDIT_BOOKS authority.
2 Implementing ElementsController the dueuno:elements API will become available
3 As a convention, all actions building and displaying a GUI are named after a verb or a name while all actions that execute a business logic are identified by a name starting with on.

Actions

An Action can implement an interactive Graphic User Interface (GUI) or act as an entry point to do some business logic and, if needed, update the user interface.

We don’t implement the business logic directly into actions, we do it into Grails Services, following Grails conventions and best practices.

To display a GUI we need to build one using Contents and Components. In the following example we create a GUI to list, create and edit books:

grails-app/controllers/BookController.groovy
@Secured(['ROLE_CAN_EDIT_BOOKS'])
class BookController implements ElementsController {

    BookService bookService (1)

    def index() {
        def c = createContent(ContentList) (2)

        c.table.with {
            columns = [
                    'title',
                    'author',
            ]
            body = bookService.list()
        }

        display content: c
    }

    private buildForm(Map obj = null) {
        def c = obj (3)
                ? createContent(ContentEdit)
                : createContent(ContentCreate)

        c.form.with {
            addField(
                    class: TextField,
                    id: 'title',
            )
            addField(
                    class: TextField,
                    id: 'author',
            )
        }

        if (obj) {
            c.form.values = obj
        }

        return c
    }

    def create() {
        def c = buildForm()
        display content: c, modal: true
    }

    def edit() {
        def book = bookService.get(params.id)
        def c = buildForm(book)
        display content: c, modal: true
    }
}
1 The BookService service implements the business logic
2 createContent() instantiates one of the available Contents to display a list of records
3 Each action ends with a display statement that renders the composed GUI to the browser
4 The GUI we build for the create and edit actions is the same. We make sure to use the appropriate content for creating and editing (See Contents)

We implement a BookService service with CRUD operations to manage a simple in memory database.

grails-app/services/BookService.groovy
class BookService {

    private static final data = [
            [id: 1, title: 'The Teachings of Don Juan', author: 'Carlos Castaneda', description: 'This is a nice fictional book'],
            [id: 2, title: 'The Antipodes of the Mind', author: 'Benny Shanon', description: 'This is a nice scientific book'],
    ]

    List<Map> list() {
        return data
    }

    Map get(Serializable id) {
        return data.find { it.id == id }
    }

    void create(Map record) {
        record.id = data.size() + 1
        data.add(record)
    }

    void update(Map record) {
        if (!record.id) throw new Exception("'id' required to update a record!")
        Map item = data.find { it.id == record.id }
        if (item) {
            item.title == record.title
            item.author = record.author
        }
    }

    void delete(Serializable id) {
        data.removeAll { it.id == id }
    }
}

Book listing:

Book listing

Editing a book:

Editing a book

Validation

Input from the user must be validated before we can save it. We can use the standard Gails Validation to make this happen.

For the purpose of this document we are going to use the Validateable Trait to check that the fields are not null and the title is unique. Please refer to the Grails Validation documentation to see all possible options.

grails-app/controllers/BookValidator.groovy
class BookValidator implements Validateable {

    String title
    String author

    BookService bookService

    static constraints = {
        title validator: { Object val, BookValidator obj, Errors errors ->
            if (obj.bookService.getByTitle(val)) {
                errors.rejectValue('title', 'unique')
            }
        }
    }
}

When rejecting values you can use the following default messages:

Code Message

range.toosmall

Value between {3} and {4}

range.toobig

Value between {3} and {4}

matches.invalid

Does not match pattern [{3}]

notEqual

Cannot be {3}

not.inList

Choose one of {3}

max.exceeded

Maximum value {3}

maxSize.exceeded

Maximum size {3}

min.notmet

Minimum value {3}

minSize.notmet

Minimum size {3}

url.invalid

Not a valid URL

email.invalid

Not a valid e-mail

creditCard.invalid

Not a valid card number

unique

Already exists

nullable

Required

blank

Required

We can now implement the whole CRUD interface:

grails-app/controllers/BookController.groovy
class BookController implements ElementsController {

    BookService bookService

    def index() {
        def c = createContent(ContentList)

        c.table.with {
            columns = [
                    'title',
                    'author',
            ]
            body = bookService.list()
        }

        display content: c
    }

    private buildForm(Map obj = null) {
        def c = obj
                ? createContent(ContentEdit)
                : createContent(ContentCreate)

        c.form.with {
            addField(
                    class: TextField,
                    id: 'title',
            )
            addField(
                    class: TextField,
                    id: 'author',
            )
        }

        if (obj) {
            c.form.values = obj
        }

        return c
    }

    def create() {
        def c = buildForm()
        display content: c, modal: true
    }

    def onCreate(BookValidator obj) { (2)
        if (obj.hasErrors()) {
            display errors: obj (1)
            return
        }

        bookService.create(params)
        display action: 'index'
    }

    def edit() {
        def book = bookService.get(params.id)
        def c = buildForm(book)
        display content: c, modal: true
    }

    def onEdit(BookValidator obj) { (2)
        if (obj.hasErrors()) {
            display errors: obj (1)
            return
        }

        bookService.update(params)
        display action: 'index'
    }

    def onDelete() { (2)
        try {
            bookService.delete(params.id)
            display action: 'index'

        } catch (Exception e) {
            display exception: e
        }
    }
}
1 We use our BookValidator class to make sure the fields are not null and the title is unique and, in case, highlight the invalid fields
2 The name of these methods is defined by the ContentList, ContentCreate and ContentEdit contents, you can see them in your browser hovering the mouse over the Delete, Create and Save buttons (look the bottom left of your browser to see which URL is going to be called when clicking the buttons)
Book listing

Internationalization (i18n)

When building the GUI, dueuno:elements automatically suggests labels for any relevant component requiring a text. To translate those labels we just copy them to its corresponding grails-app/i18n/messages_*.properties file giving them a translation.

For example to enable the English and Italian languages we can do as follows.

English:

grails-app/i18n/messages.properties.groovy
shell.book=Books
shell.read=Read
book.index.header.title=Books
book.create.header.title=New Book
book.edit.header.title=Book
book.title=Title
book.author=Author
Book listing
Editing a book

Italian:

grails-app/i18n/messages_it.properties.groovy
shell.book=Libri
shell.read=Leggi
book.index.header.title=Libri
book.create.header.title=Nuovo libro
book.edit.header.title=Libro
book.title=Titolo
book.author=Autore
Book listing
Editing a book

The User Menu will automatically display the available languages based on the presence of their language files.

Available languages

display()

The most relevant feature of dueuno:elements is the display method. It renders the GUI on the server and sends is to the browser.

You can call display with one or more of the following parameters:

Name Type Description

controller

String

The name of the controller to redirect to. If no action is specified the index action will be displayed

action

String

The name of the action to redirect to. If no controller is specified the current controller will be used

params

Map<String, Object>

The params to pass when redirecting to a controller or action

content

PageContent

The content to display (See Contents)

transition

Transition

The transition to display (See Transitions)

modal

Boolean

Whether to display the content in a modal dialog or not

wide

Boolean

When displaying the content as modal the dialog will be wider.

fullscreen

Boolean

When displaying the content as modal the dialog will fit the whole browser window size.

closeButton

Boolean

When displaying the content as modal the dialog will present a close button on the top-left side to let the user close the dialog cancelling the operation (Default: true).

errors

org.springframework.validation.Errors

Validation errors to display (See Validation)

errorMessage

String

Message to display in a message box to the user

exception

Exception

Exception to display in a message box to the user

message

String

Message to display in a message box to the user

Transitions

A Transition is a set of instructions sent from the server to the client (browser) to alter the currently displayed content. For instance, when selecting a book from a list we want a text field to be populated with its description. To implement such behaviours we use transitions.

Please refer to Controls and Components to see what events are available to each component.
Refer to Websockets to understand how to trigger events programmatically from sources other than the user input.
grails-app/controllers/ReadController.groovy
class ReadController implements ElementsController {

    BookService bookService

    def index() {
        def c = createContent(ContentForm)

        c.header.removeNextButton()

        c.form.with {
            addField(
                    class: Select,
                    id: 'book',
                    optionsFromRecordset: bookService.list(),
                    onChange: 'onChangeBook', (1)
            )
            addField(
                    class: Textarea,
                    id: 'description',
            )
        }

        display content: c
    }

    def onChangeBook() {
        def t = createTransition() (2)
        def book = bookService.get(params.book)

        if (book) {
            t.set('description', book.description) (3)
            t.set('description', 'readonly', true) (4)
        } else {
            t.set('description', null)
            t.set('description', 'readonly', false)
        }

        display transition: t
    }
}
1 We tell the Select field which action to execute when the change event occurs (See Events)
2 We create a new Transition
3 The set method sets the value of the description field
4 We also set the Textarea to a readonly state
onChange transition

To finish it up we register a Pretty Printer for the book record and tell the 'Select' control to use it to display the items.

grails-app/init/BootStrap.groovy
class BootStrap {

    ApplicationService applicationService

    def init = { servletContext ->
        applicationService.init {

            registerPrettyPrinter('BOOK', '${it.title} - ${it.author}') (1)

        }
    }
}
1 A pretty printer called BOOK will display each book by title and author. The it variable refers to an instance of the book record (a Map in this case)
grails-app/controllers/ReadController.groovy
class ReadController implements ElementsController {
    ...

        addField(
                class: Select,
                id: 'book',
                optionsFromRecordset: bookService.list(),
                prettyPrinter: 'BOOK', (1)
                onChange: 'onChangeBook',
        )

    ...
}
1 We configure the Select control to use the BOOK pretty printer to format the books
onChange transition

Exceptions

When developing the application all unhandled exceptions will be rendered to the browser as follows.

In production, all the details will be hidden and just the sad face will be displayed.
onChange transition

To display a message box instead you can add an Exception handler to the controller:

grails-app/controllers/ReadController.groovy
class ReadController implements ElementsController {

    def handleException(Exception e) {
        display exception: e
    }

    def handleMyCustomException(MyCustomException e) {
        display exception: e
    }

}
onChange transition

Contents

Contents are the canvas to each feature. You can create a ContentBlank, which is a plain empty canvas, and add Components to it. This is not something you will usually want to do since dueuno:elements provides pre-assembled contents to be used right away.

Components are added to the content on a vertical stripe one after the other. We can not layout components, to create a layout we need to use the Form component or we can create a custom component.

ContentBase

Embeds a Header and a Confirm Button that submits a component called form (not provided) to an action called onConfirm.

ContentForm

Extends ContentBase and embeds a Form called form.

ContentCreate

Extends ContentForm and provides a Create Button that submits the form component to an action called onCreate.

ContentEdit

Extends ContentForm and provides a Save Button that submits the form component to an action called onEdit.

ContentList

Extends ContentBase and embeds a Table component. Provides a New Button that redirects to an action called create.

The Table component is configured to present and Edit and a Delete Button for each displayed row. The Edit Button submits the raw id to an action called edit while the Delete Button asks for confirmation before redirecting to an action called onDelete.

Components

Everything in dueuno_elements is a Component. A component is itself a tiny web application. Each component is built with at least an HTML view, a CSS styling and a JavaScript logic. A Component can provide a supporting Service or Controller.

Unless we want to create a new component there is no need to know HTML, CSS or JavaScript to develop a dueuno:elements application.

Each component extends the base class Component so each component share the following properties and methods.

Properties

Property Type Description

id

String

Id of the component instance. This is mandatory, it must be unique and provided in the constructor.

visible

Boolean

Shows or hides the component without changing the layout (Default: true)

display

Boolean

Displays or hides the component, adding or removing it from the layout (Default: true)

readonly

Boolean

Readonly controls are disabled (Default: false)

skipFocus

Boolean

The component won’t participate in keyboard or mouse selection (focus) (Default: false)

sticky

Boolean

The component is sticky on top

containerSpecs

Map

Contains instructions for the container. The container component may or may not respect them, see the documentation for the specific container component.

textColor

String

The text color, CSS format

backgroundColor

String

Background color, CSS format

cssClass

String

Custom CSS class to apply. The CSS class must be a Bootstrap] CSS class or a cusom one declared into the grails-app/assets/dueuno/custom/application.css file. See Custom CSS

cssStyle

String

Custom CSS inline style.

Methods

Method Description

addComponent(Map)

Adds a component as children. See Components.

addControl(Map)

Adds a control as children. See Controls.

on(Map)

Configures an event. See Events.

Header

A Header is a bar at the top of the Content area. It can be sticky on top or it can scroll with the content. Its main purpose is to hold navigation buttons.

A Header can have a backButton on the left and a nextButton on the right. In the middle we can find the title.

Properties

Property Type Description

sticky

Boolean

When set to true the header will stick on top. When a backButton or nextButton is added to the header than sticky is automatically set to true to let the user reach the buttons even if the content has been scrolled down. To force the header to scroll with the content explicitly set sticky to false.

title

String

The title to display

titleArgs

List

Args to be used when indexing an i18n message. Eg: in messages.properties exists the following property book.index.header.title=Books for {0} {1} and titleArgs = ['Mario', 'Rossi']. The title will result in Books for Mario Rossi.

icon

String

An icon to be displayed before the title. We can choose one from Font Awesome

hasBackButton

Boolean

true if a backButton has been added

hasNextButton

Boolean

true if a nextButton has been added

backButton

Button

The back button object. See Button

nextButton

Button

The next button object. See Button

Methods

Method Description

addBackButton(Map)

Add the backButton. Accepts the arguments of Button

removeBackButton()

Removes the backButton.

addNextButton(Map)

Add the nextButton. Accepts the arguments of Button

removeNextButton()

Removes the nextButton.

Table

A Table is a convenient way to display a recordset.

Each table can implement some TableFilters and each row can have its own set of action buttons. For each row, depending on the logged in user and the status of the record we can define which actions are available.

Properties

Property Type Description

columns

List<String>

A list of column names to display. Each column name must match the recordset column name to automatically display its values.

    c.table.with {
        columns = [
            'title',
            'author',
        ]
    }

keys

List<String>

List of key names. When specified, a new column will be created for each key. The keys will be automatically submitted when a row action is activated.

c.table.with {
    keys = [
        'publisher_id',
    ]
}

sortable

Map<String, String>

Defines the sortable columns

c.table.with {
    sortable = [
        title: 'asc',
    ]
}

sort

Map<String, String>

Defines the sorting of the recordset. It takes precedence over the sortable property and forces the specified sorting.

c.table.with {
    sort = [
        title: 'asc',
    ]
}

submit

List<String>

The name of the column names whose values must be included when the table is submitted by a Button or Link.

c.table.with {
    submit = [
        'author',
    ]
}

labels

Map<String, String>

Programmatically change the label of the specified columns.

c.table.with {
    labels = [
        author: '-',
    ]
}

transformers

Map<String, String>

Sets a transformer to a column. Each value of that column will be processed by the specified transformer (See registerTransformer())

c.table.with {
    transformers = [
        title: 'UPPERCASE_TITLE',
    ]
}

prettyPrinters

Map<String, Object>

Sets a pretty printer to a column. Each value of that column will be processed by the specified pretty printer (See registerPrettyPrinter())

c.table.with {
    prettyPrinter = [
        title: 'UPPERCASE_TITLE',
    ]
}

prettyPrinterProperties

Map<String, Map>

Sets some pretty printer properties to a column. Each value of that column will be processed by the specified properties (See PrettyPrinterProperties)

c.table.with {
    prettyPrinterProperties = [
        salary: [
            highlightNegative: false,
            renderZero: '-',
        ],
        name: [
            renderMessagePrefix: true,
        ],
    ]
}

stickyHeader

Boolean

If true the table header will stick to top when scrolling. Not available in modals (Default: true)

filters

TableFilters

To define table filters:

c.table.with {
    filters.with {
        addField(
            class: TextField,
            id: 'title',
            cols: 6,
        )
        addField(
            class: TextField,
            id: 'author',
            cols: 6,
        )
    }
}

Map filters = c.table.filters.values (1)
1 The submitted values of the filters fields. See TableFilters

displayActions

Boolean

Whether to display the row action buttons or not (Default: true)

displayHeader

Boolean

Whether to display the table header or not (Default: true)

displayFooter

Boolean

Whether to display the table footer or not (Default: true)

displayPagination

Boolean

Whether to display the table pagination or not (Default: true)

enableComponents

Boolean

Whether to render the table to host custom components on its cells or not. Enabling this feature slows down the rendering (Default: false)

rowHighlight

Boolean

Whether to highlight the rows on mouse pointer hover (Default: true)

rowStriped

Boolean

Whether to set the zebra style or not (Default: false)

noResults

Boolean

Whether to display a box with an icon and a text when the table has no results (Default: true)

noResultsIcon

String

The icon ti display when the table has no results. Choose one from Font Awesome.

noResultsMessage

String

The message to display when the table has no results.

Methods

Method Description

body

Assigns a recordset to the table body (See Recordsets)

c.table.body = bookService.list()

footer

Assigns a recordset to the table footer (See Recordsets)

c.table.footer = bookService.listTotals()

paginate

If set the table will paginate the results. Must be set to the total count of the records to show.

c.table.paginate = bookService.count()

eachRow

This closure gets called for each row displayed by the table. Don’t execute slow code here since it will slow down the whole table rendering.

c.table.body.eachRow { TableRow row, Map values -> (1)
    row.cells['title'] (2)
    row.actions (3)
}
1 The record values
2 See Label
3 See Row Actions

Recordsets

What can we load a table with?

List of Lists

Loading a table with a List of Lists is possible, the sequence will determine how each column will be mapped to each value. There is no hard relationship between the displayed column name and the value.

For this reason we suggest using List of Maps instead.

c.table.columns = [
    'title',
    'author',
    'description',
]

c.table.body = [
    ['The Teachings of Don Juan', 'Carlos Castaneda', 'This is a nice fictional book'],
    ['The Antipodes of the Mind', 'Benny Shanon', 'This is a nice scientific book'],
]
List of Maps

We can load a table with a "recordset" style data structure like the List of Maps. This way each column will display exactly the value associated to the key of the record (Map) having the same name of the column.

c.table.columns = [
    'title',
    'author',
    'id',
]

c.table.body = [
    [id: '1', title: 'The Teachings of Don Juan', author: 'Carlos Castaneda', description: 'This is a nice fictional book'],
    [id: '2', title: 'The Antipodes of the Mind', author: 'Benny Shanon', description: 'This is a nice scientific book'],
]
List of POGOs

A List of Plain Old Groovy Objects can also be used to load a table.

Given this POGO:

class Book {
    String id
    String title
    Strng author
    String description
}

We can load our table:

c.table.columns = [
    'title',
    'author',
    'id',
]

c.table.body = [
    new Book(id: '1', title: 'The Teachings of Don Juan', author: 'Carlos Castaneda', description: 'This is a nice fictional book'),
    new Book(id: '2', title: 'The Antipodes of the Mind', author: 'Benny Shanon', description: 'This is a nice scientific book'),
]
GORM Recordsets

Using a GORM Recordset is an easy way to load a table. See how to build a CRUD.

c.table.columns = [
    'title',
    'author',
]

c.table.body = TBook.list()
c.table.paginate = TBook.count()

Row Actions

There are two ways to configure row actions. All at once and on a row basis. To set all rows to have the same actions we can set them up in the table namespace as follows:

c.table.with {
    columns = [
        'title',
        'author',
    ]
    actions.addAction(action: 'borrow') (1)
    actions.addAction(action: 'return')
}
1 See Button for all the Button properties

If we need to configure the row actions depending on the record values or other logics we can do it from the eachRow closure.

c.table.with {
    columns = [
        'title',
        'author',
    ]

    body.eachRow {
        if (values.borrowed) {
            row.actions.addAction(action: 'return') (1)
        } else {
            row.actions.addAction(action: 'borrow')
        }
    }
}
1 See Button for all the Button properties

Group Actions

The table can be configured to select multiple rows ad apply to all of them the same action.

c.table.with {
    columns = [
        'title',
        'author',
    ]

    groupActions.addAction(action: 'return') (1)
    groupActions.addAction(action: 'borrow')
}
1 See Button for all the Button properties

TableFilters

Each table can have its own search Form to filter results. When submitting the filters, the action containing them will be reloaded and the filters values will be available in the Grails params map.

c.table.with {
    filters.with {
        addField(
            class: Select,
            optionsFromRecordset: bookService.list(),
            prettyPrinter: 'BOOK',
            id: 'book',
            cols: 4,
        )
        addField(
            class: TextField,
            id: 'search',
            cols: 8,
        )
    }

    Map filters = c.table.filters.values (1)
}
1 The submitted values of the filters fields.

Properties

Property Type Description

isFiltering

Boolean

true if the filters form has values in its fields

fold

Boolean

Whether the filters form is displayed as folded or not at its first appearance. After that its folded state will be stored in the session (Default: true)

autoFold

Boolean

If set to true the filters form will be folded each time a search is submitted (Default: false)

Methods

Method Description

addField()

Adds a form field. See FormField and Controls

Form

A form is the component we use to layout Components and Controls. Form implements the grid system, once activated we have 12 columns we can use to arrange form fields horizontally.

When the application is accessed from a mobile phone all the fields will be displayed in a single column. This makes them usable when the available space is not enough to organise them in a meaningful way.

c.form.with {
    grid = true
    addField(
        class: TextField,
        id: 'title',
        cols: 6,
    )
    addField(
        class: TextField,
        id: 'author',
        cols: 6,
    )

    values = [ (1)
        title: 'This is a book',
        author: 'This is an author',
    ]
}
1 Assigning the values to a form, in this case with a Map

Properties

Property Type Description

validate

Class

A grails.validation.Validateable class or a GORM domain class used to automatically render the field as required. A red * will be displayed next to the field label if appropriate.

grid

Boolean

Whether to activate the grid system or not (Default: false)

readonly

Boolean

Sets all the form fields readonly (Default: false)

Methods

Method Description

addField(Map)

Adds a form field. See FormField and Controls

setValues(Object)

Fills the form with the values of a GORM domain object instance, a POGO instance or a Map

FormField

A form field wraps a Control with a label and sets it into the grid system. A FormField is automatically created each time we add a field to a Form calling its addField() method.

Properties

Property Type Description

component

Component

The contained component

label

String

The field label

labelArgs

List

A list of objects to pass to the localized message (Eg. when using {0} in message.properties)

helpMessage

String

A help message

helpMessageArgs

List

A list of objects to pass to the localized message (Eg. when using {0} in message.properties)

nullable

Boolean

Whether to display the field as nullable or not. If set will override the form validate logic (See Form) (Default: true)

displayLabel

Boolean

If set to false the label will not be displayed. The space occupied by the label will be taken off the screen resulting in a different vertical positioning of the Control.

cols

Integer

Defines how many columns of the grid system will be used to span the Control to. Its value must be between 1 and 12 included.

rows

Integer

If the Control is a multiline one we can set how many lines it is going to occupy.

Button

Buttons are key components of the GUI. We use buttons to let the user trigger actions. The Button component can provide the user with multiple actions to be executed.

A single button can display two directly accessible actions, the defaultAction and tailAction and a menu with a list of links, the actionMenu.

defaultAction tailAction actionMenu

A simple button will have just the defaultAction.

c.form.with {
    def addBookField = addField( (1)
        class: Button,
        id: 'addBook',
        action: 'addBook',
        submit: ['form'],
    )

    def button = addBookField.component
    button.addAction(controller: 'addAuthor')
}
1 A Button can be initialized with the properties of an event (See Events and Link (See Link)

Properties

Property Type Description

defaultAction

Menu

The default action

tailAction

Menu

The tail action

actionMenu

Menu

The action menu

primary

Boolean

When set to true the button color will use the PRIMARY_BACKGROUND_COLOR and PRIMARY_TEXT_COLOR tenant properties indicating that its role in the GUI is primary (See Tenant Properties) (Default: false).

stretch

Boolean

Set to true to let the button fill all the available horizontal space (Default: false).

group

Boolean

If set to true all actions of the button will be displayed inline and directly accessible (Default: false).

maxWidth

Integer

The max width in pixels that the button can reach.

Events

Event Description

click

The event is triggered on mouse click or finger tab on touch devices

A menu is the component we use to organize the Shell and Button menus. It can hold a tree of items with a parent-children structure but we use only one level to group items (See Features).

This component is meant for internal use only.

Links are everywhere, they are in the Shell menus, in Buttons actions, TextField or Select actions, and they can be used as stand alone. Links and buttons share the same properties.

c.form.with {
    addField( (1)
        class: Link,
        id: 'addBook',
        action: 'addBook',
        submit: ['form'],
        icon: 'fa-book',
    )
}
1 A Link can be initialized with the properties of a Label and an event (See Events)

Properties

Property Type Description

icon

String

Icon that graphically represents the link. Choose one from Font Awesome.

image

String

An SVG image that graphically represents the link. If specified a corresponding file must exist in the grails-app/assets folder.

text

String

A label that describes the link, usually a code found in messages.properties

direct

Boolean

Whether to render the whole html page (or raw http body) or a Transition

target

String

Set a target name to open the page into a new browser tab. All links with te same target will display in the same tab.

targetNew

Boolean

If set to true the link will display on a new tab each time it is clicked

modal

Boolean

Whether to display the content in a modal dialog or not

wide

Boolean

When displaying the content as modal the dialog will be wider.

fullscreen

Boolean

When displaying the content as modal the dialog will fit the whole browser window size.

closeButton

Boolean

When displaying the content as modal the dialog will present a close button on the top-left side to let the user close the dialog cancelling the operation (Default: true).

updateUrl

Boolean

If set to true the browser address bar will be updated with the link destination URL, otherwise the browser will not update its address bar (Default: false) NOTE: Accessing from a mobile phone the address bar will never be updated to enhance the user experience.

animate

String

Can be set to fade, next and back. At the moment only fade is implemented as a graphical transaction when changing content.

infoMessage

String

If specified an info message will pop up, the link will never be executed

confirmMessage

String

If specified a confirm message will pop up giving the user a chance to cancel the action

Events

Event Description

click

The event is triggered on mouse click or finger tap on touch devices

Label

c.form.with {
    addField(
        class: Label,
        id: 'label',
        html: '<b>This is a bold statement!</b>',
        textAlign: TextAlign.END,
        textWrap: TextWrap.LINE_WRAP,
    )
}

Properties

Property Type Description

text

Object

The text to display. If it’s a Boolean value a check will be displayed.

html

String

An html string, useful to format text or insert links

url

String

If specified the text will be a link to this URL

icon

String

An icon to display before the text, you can choose one from Font Awesome

textAlign

TextAlign

Determines the text horizontal alignment. It can be set to DEFAULT, START, END or CENTER (Default: DEFAULT).

textWrap

TextWrap

Determines how the text is wrapped:

  1. NO_WRAP The text will be displayed in one line

  2. SOFT_WRAP The text will wrap when the max width of the container is reached. Lines breaks are NOT considered.

  3. LINE_WRAP Each line will be displayed in one line until the max width of the container is reached. Line breaks are taken in consideration.

  4. LINE_BREAK Each line will be displayed in one line. Line breaks are taken in consideration.

monospace

Boolean

Use a monospaced font instead of the default one

border

Boolean

Draws a coloured background. Useful when we want to display the label in a different color.

renderBoolean

Boolean

If true a check symbol will be displayed, otherwise the text true or false will be displayed (Default: true).

Separator

Wa can use separators to space between a set of fields and another one in a form.

Properties

Property Type Description

squeeze

Boolean

Reduces the space the separator will introduce leaving just the space for the label

KeyPress

We use the KeyPress component to intercept key pressed by the user on the GUI. Its main use is to integrate barcode readers but it can be used for any other scenario.

def c = createContent(ContentList)
c.addComponent(
    class: KeyPress,
    id: 'keyPress',
    action: 'onKeyPress', (1)
)
1 See Events to configure the event

Properties

Property Type Description

triggerKey

String

Key pressed are stored into a buffer until a trigger key is pressed. When this happens the configured event is called. The trigger key can be any character or Enter. If set to blank '' each key pressed will be immediately sent (Default: Enter).

Controls

Controls are Components that can hold a value. Controls are the main way to interact with the application. We mainly use controls in forms to easily submit their values.

Here is the basic syntax to create a control:

c.form.addField(
    class: TextField, (1)
    id: 'text', (2)
    defaultValue: 'Hello!', (3)
    value: 'Hello World!', (4)
)
1 The control class to instantiate
2 The control id
3 A default value. If specified, the control will be assigned with this value when no value is specified
4 If specified, the control will be assigned with this value

When submitted to an action, the params map will contain an entry with the name of the control and its value like [text: 'Hello World!'] in this case.

TextField

A text field.

c.form.addField(
    class: TextField,
    id: 'username',
    icon: 'fa-user',
)

Properties

Property Type Description

icon

String

An icon to display within the control, you can choose one from Font Awesome

prefix

String

A text to display before the edit area of the control

maxSize

Integer

Max number of characters the user can input

placeholder

String

A text to display when the text area is empty

monospace

yyy

Use a monospaced font instead of the default one

textTransform

TextTransform

Transforms the input while typing. It may be one of the following:

  1. UPPERCASE

  2. LOWERCASE

  3. CAPITALIZE each word

pattern

String

A RegEx pattern to accept only specific input (Eg. '^[0-9\\.\\,]*$' will accept only numbers, dots and columns)

Methods

Method Description

addAction()

Adds an action button at the end of the control. See Link.

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

Select

Displays a list of options to choose from.

Properties

Property Type Description

optionsFromRecordset

List<Map> or List<Object> or GORM Recordset

Options will be set from the recordset

optionsFromList

List

Options will be set from the List items. The key of each item will match the value of the item itself.

optionsFromEnum

Enum

Options will be set from the Enum. The key of each item will match the value of the item itself.

options

Map

Options will be set from the Map items (key/value)

keys

List<String>

List of column names to submit as the key for the selected option (Default: ['id'])

prettyPrinter

Class or String

Use the specified pretty printer to display the options. See registerPrettyPrinter(). If the registered pretty printer Class matches the item class, the pretty printer will be automatically applied.

transformer

String

Name of the transformer to use to display the options. See registerTransformer()

messagePrefix

String

Prefix to add to each item so it can be referred in message.properties files to localise it

renderMessagePrefix

Boolean

Whether to display the messagePrefix or not (Default: true)

placeholder

String

Displays a text when no option is selected

allowClear

Boolean

If true the selection can be cleared

autoSelect

Boolean

When there is only one available option in the list it will be automatically selected (Default: true)

multiple

Boolean

Enables multiple selections (Default: false)

search

Boolean

Displays a search box to filter the available options. It works on the client side, to search on the server we need to user the search event.

monospace

Boolean

Use a monospaced font instead of the default one

searchMinInputLength

Integer

Minimum number of characters to input before the search on the server can start. Works in combination with the search event.

Methods

Method Description

Select.optionsFromRecordset(recordset: …​)

Returns a Map of options to be used in a transition. See Search on server. Accepts a Map, you can set the following arguments: keys, keysSeparator, prettyPrinter, transformer messagePrefix, renderMessagePrefix, locale.

Select.optionsFromList(list: …​)

Returns a Map of options to be used in a transition. See Search on server. Accepts a Map, you can set the above arguments.

Select.optionsFromEnum(enum: …​)

Returns a Map of options to be used in a transition. See Search on server. Accepts a Map, you can set the above arguments.

Select.options(options: …​)

Returns a Map of options to be used in a transition. See Search on server. Accepts a Map, you can set the above arguments.

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

search

Triggered when searchMinInputLength is reached

Example of setting up a server search.

c.form.with {
    addField(
        class: Select,
        id: 'activity',
        onLoad: 'onActivityLoad', (1)
        onChange: 'onActivityChange',
        onSearch: 'onActivitySearch', (2)
        searchMinInputLength: 0, (3)
        submit: ['form'],
        allowClear: true,
    )
}
1 The load event must return a single option to display
2 The search event will return a list of matching options
3 if 0 then the search event will be triggered as soon as the user clicks on the control to open the options list.

We need to create the following actions.

ActivityService activityService

def onActivityLoad() {
    def t = createTransition()
    def activities = activityService.list(id: params.activity) (1)
    def options = Select.optionsFromRecordset(recordset: activities)
    t.set('activity', 'options', options)
    display transition: t
}

def onActivityChange() {
    def t = createTransition()
    // Do something...
    display transition: t
}

def onActivitySearch() {
    def t = createTransition()
    def activities = activityService.list(find: params.activity) (2)
    def options = Select.optionsFromRecordset(recordset: activities)
    t.set('activity', 'options', options)
    display transition: t
}
1 params.activity will hold the selected id
2 params.activity will hold the search string

Checkbox

A checkbox is a way to interact with Boolean values.

c.form.with {
    addField(
        class: Checkbox,
        id: 'fullscreen',
        displayLabel: false,
        cols: 3,
    )
}

Properties

Property Type Description

text

String

The text to display

Events

Event Description

click

Not implemented yet

MultipleCheckbox

Manage multiple checkboxes as it was a Select control with many options. See Select.

Textarea

A text area who can span multiple lines of a form.

c.form.with {
    addField(
        class: Textarea,
        id: 'textarea',
        maxSize: 100,
        cols: 12,
        rows: 5,
    )
}

Properties

Property Type Description

maxSize

Integer

Max number of characters the user can input

monospace

Boolean

Use a monospaced font instead of the default one

Events

Event Description

change

Triggered when the value changes

QuantityField

A text field to input quantities.

c.form.with {
    addField(
        class: QuantityField,
        id: 'quantity',
        defaultUnit: QuantityUnit.KM,
        availableUnits: quantityService.listAllUnits(),
    )
}

Properties

Property Type Description

decimals

Integer

How many decimal digits are allowed (Default: 2).

negative

Boolean

If negative values are allowed (Default: false).

unitOptions

List

A list of units to select from

defaultUnit

QuantityUnit

The default unit to display

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

MoneyField

A text field to input currency values.

c.form.with {
    addField(
        class: MoneyField,
        id: 'salary',
        decimals: 0,
    )
}

Properties

Property Type Description

decimals

Integer

How many decimal digits are allowed (Default: 2).

negative

Boolean

If negative values are allowed (Default: false).

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

NumberField

A text field to manage number values.

c.form.with {
    addField(
        class: NumberField,
        id: 'number',
        min: -2,
        max: 10,
    )
}

Properties

Property Type Description

decimals

Integer

How many decimal digits are allowed (Default: 2).

negative

Boolean

If negative values are allowed (Default: false).

min

Integer

Minimum number the user can input

max

Integer

Maximum number the user can input

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

DateField

A control to input a date.

c.form.with {
    addField(
        class: DateField,
        id: 'dateStart',
        min: LocalDate.now().minusDays(3),
        max: LocalDate.now().plusDays(3),
    )
}

Properties

Property Type Description

min

LocalDate

Minimum date the user can input

max

LocalDate

Maximum date the user can input

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

TimeField

A control to input a time.

c.form.with {
    addField(
        class: TimeField,
        id: 'time',
        min: LocalTime.now().minusHours(3),
        timeStep: 10,
    )
}

Properties

Property Type Description

min

LocalTime

Minimum time the user can input

max

LocalTime

Maximum time the user can input

timeStep

Integer

The amount of minutes the user can select. For example if set to 15 the only available time selections are 00, 15, 30 and 45.

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

DateTimeField

A control to input a date and time.

c.form.with {
    addField(
        class: DateTimeField,
        id: 'datetime',
        min: LocalDate.now().minusDays(3),
    )
}

Properties

Property Type Description

min

LocalDate

Minimum date the user can input

max

LocalDate

Maximum date the user can input

timeStep

Integer

The amount of minutes the user can select. For example if set to 15 the only available time selections are 00, 15, 30 and 45.

Events

Event Description

load

Triggered once the content is loaded

change

Triggered when the value changes

EmailField

A control to input an email. See TextField.

c.form.with {
    addField(
        class: EmailField,
        id: 'email',
    )
}

TelephoneField

A control to input a telephone number. See TextField.

c.form.with {
    addField(
        class: TelephoneField,
        id: 'telephone',
    )
}

UrlField

A control to input a URL. See TextField.

c.form.with {
    addField(
        class: UrlField,
        id: 'url',
    )
}

PasswordField

A control to input a password. See TextField.

c.form.with {
    addField(
        class: PasswordField,
        id: 'password',
    )
}

HiddenField

A control to store a value without displaying it to the user.

c.form.with {
    addField(
        class: HiddenField,
        id: 'hidden',
        value: 'This is not visible but it will be submitted',
    )
}

Events

Each Component can trigger one or more events. Please see Components and Controls to see what events each specific component can trigger.

Each available event has a lowercase name. We can configure the event directly when creating a component as follows.

c.form.with {
    addField(
        class: Select,
        id: 'book',
        onChange: 'onChangeBook', (1)
        submit: ['form'],
    )
}
1 The parameter name is composed by on followed by the capitalized name of the event (the event change in this case). The parameter value is the name of the action to be called.

Multiple events can be configured as follows.

c.form.with {
    def books = addField(
        class: Select,
        id: 'book',
    ).component (1)

    books.with {
        on( (2)
            event: 'load',
            action: 'onLoadBooks',
        )
        on( (3)
            event: 'change',
            action: 'onChangeBook',
            submit: ['form'],
        )
    }
}
1 We reference the component hold by the FormField, not the form field itself
2 Configuring the load event
3 Configuring the change event

The following properties can be specified when configuring an event on a component.

Properties

Property Type Description

controller

String

The name of the controller to redirect to. If no action is specified the index action will be displayed

action

String

The name of the action to redirect to. If no controller is specified and we are in the context of a web request (Eg. it’s a user triggered event) the current controller will be used. If we are configuring the event outside of a web request (Eg. sending an event from a job) a controller must be specified.

params

Map<String, Object>

The params to pass when redirecting to a controller or action

submit

List<String>

Name list of the components whose values we want to submit. Each component is responsible to define the data structure for the values it contains. The default behaviour will send the values of all the controls contained within the component.

PrettyPrinterProperties

Every value in dueuno:elements gets displayed by the PrettyPrinter subsystem. Components and Controls can be configured to override the user settings and the system settings. Refer to the documentation of each component to see how those settings can be configured.

Name Type Description

prettyPrinter

Object

Class or String name of the pretty printer

transformer

String

Transformer name

locale

Locale

-

renderMessagePrefix

Boolean

Default: false, set to true to translate the value into message.properties files

messagePrefix

String

Add or change the message prefix

messageArgs

List

Add args for the i18n message

renderBoolean

Boolean

If false renders the text true/false otherwise renders a check symbol when true and nothing when false (Defaults: true)

highlightNegative

Boolean

If the value is < 0 the text will be highlighted in red (Default: false)

renderZero

String

If the value is 0 render the specified string instead

renderDate

Boolean

For LocalDateTime values, whether to render the DATE part or not

renderDatePattern

String

Change the way the date is rendered (See DateTimeFormatter)

renderTime

Boolean

For LocalDateTime values, whether to render the TIME part or not

renderSeconds

Boolean

For LocalTime values, whether to display the seconds or not

renderDelimiter

String

For Map and List values, use this delimiter to list the items (Default: ', ')

decimals

Integer

For Number values, how many decimals digits to display

decimalFormat

String

For Number values, which decimal separator to use. It can be ISO_COM (,) or ISO_DOT (.) (Default: ISO_COM)

prefixedUnit

Boolean

For Quantity and Money values, whether to display the unit of measure before or after the value (Default: false)

symbolicCurrency

Boolean

For Money values, whether to display the currency with a symbolic or ISO code (Default: true)

symbolicQuantity

Boolean

For Quantity values, whether to display the unit of measure with a symbolic or SI code (Default: true)

invertedMonth

Boolean

For Date values, whether to display month/day/year (true) or day/month/year (false) (Default: false)

twelveHours

Boolean

For Time values, whether to display 12H (true, uses AM/PM) or 24H (false) (Default: false)

firstDaySunday

Boolean

Whether to display Sunday as the first day of the week (true) or not (Default: false)

Websockets

TODO

Tenant Properties

TODO

System Properties

TODO

Custom CSS

TODO

Custom JavaScript

TODO