API Reference
Dueuno Elements is a Grails plugin useful to develop backoffice applications.
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:
dependencies {
…
implementation "org.dueuno:elements-core:2.0-SNAPSHOT"
}
Add the following code to initialize the application:
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:
spring:
main:
banner-mode: "log"
<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
).
URL Path
We can set up our application to be accessible from a specific URL path. For example http://localhost:8080/admin
.
class BootStrap {
ApplicationService applicationService
TenantPropertyService tenantPropertyService
def init = { servletContext ->
applicationService.onInstall { String tenantId ->
tenantPropertyService.setString('SHELL_URL_MAPPING', '/admin')
tenantPropertyService.setString('LOGIN_LANDING_URL', '/') (1)
tenantPropertyService.setString('LOGOUT_LANDING_URL', '/') (2)
}
}
}
1 | After the user logs in the shell will redirect to this path |
2 | When the user logs out the shell will redirect to this path |
We also need to configure the URL mappings to tell Grails what’s the new configuration. For example if the main controller of our website is WebsiteController.groovy
we can create the following file:
class WebsiteUrlMappings {
static mappings = {
"/"(controller: 'website')
"/admin"(controller: 'shell') (1)
}
}
1 | The configured path must match the value of the SHELL_URL_MAPPING tenant property. |
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.
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.
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.
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.
class BootStrap {
SecurityService securityService (1)
def init = { servletContext ->
securityService.afterLogin {
// ...
}
}
}
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.
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.
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.
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.
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.
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.
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.
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. |
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.
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.
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 |
Available options:
Name | Type | Description |
---|---|---|
|
|
The name of the controller that implements the feature |
|
|
(OPTIONAL) The name of the action to execute (default: |
|
|
(OPTIONAL) Parameters to add when calling the |
|
|
(OPTIONAL) List of the component names that will be processed to retrieve the values to be passed when calling the |
|
|
(OPTIONAL) Menu item icon, you can choose one from Font Awesome |
|
|
(OPTIONAL) The feature will be displayed only to the users configured with the roles in the list (default: |
|
|
(OPTIONAL) If |
|
|
(OPTIONAL) An absolute URL. When specified it takes precedence so |
|
|
(OPTIONAL) Menu items are URLs managed by Dueuno Elements. When set to |
|
|
(OPTIONAL) The feature will be displayed in a new browser tab with the provided name |
|
|
(OPTIONAL) The feature will be displayed in a new browser tab ( |
|
|
(OPTIONAL) Message to display before the feature is displayed giving the option to cancel or confirm the operation |
|
|
(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()
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.
@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:
@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.
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:
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.
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 |
---|---|
|
Value between {3} and {4} |
|
Value between {3} and {4} |
|
Does not match pattern [{3}] |
|
Cannot be {3} |
|
Choose one of {3} |
|
Maximum value {3} |
|
Maximum size {3} |
|
Minimum value {3} |
|
Minimum size {3} |
|
Not a valid URL |
|
Not a valid e-mail |
|
Not a valid card number |
|
Already exists |
|
Required |
|
Required |
We can now implement the whole CRUD interface:
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) |
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:
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
Italian:
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
The User Menu will automatically display the available languages based on the presence of their language files.
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 |
---|---|---|
|
|
The name of the controller to redirect to. If no |
|
|
The name of the action to redirect to. If no |
|
|
The params to pass when redirecting to a |
|
|
The content to display (See Contents) |
|
|
The transition to display (See Transitions) |
|
|
Whether to display the content in a modal dialog or not |
|
|
When displaying the content as |
|
|
When displaying the content as |
|
|
When displaying the content as |
|
|
Validation errors to display (See Validation) |
|
|
Message to display in a message box to the user |
|
|
Exception to display in a message box to the user |
|
|
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. |
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 |
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.
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) |
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 |
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.
|
To display a message box instead you can add an Exception handler to the controller:
class ReadController implements ElementsController {
def handleException(Exception e) {
display exception: e
}
def handleMyCustomException(MyCustomException e) {
display exception: e
}
}
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 ContentHeader
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 ContentHeader
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 of the component instance. This is mandatory, it must be unique and provided in the constructor. |
|
|
Shows or hides the component without changing the layout (Default: |
|
|
Displays or hides the component, adding or removing it from the layout (Default: |
|
|
Readonly controls are disabled (Default: |
|
|
The component won’t participate in keyboard or mouse selection (focus) (Default: |
|
|
The component is sticky on top |
|
|
Contains instructions for the container. The container component may or may not respect them, see the documentation for the specific container component. |
|
|
The text color, CSS format |
|
|
Background color, CSS format |
|
|
Custom CSS class to apply. The CSS class must be a Bootstrap] CSS class or a cusom one declared into the |
|
|
Custom CSS inline style. |
Methods
Method | Description |
---|---|
|
Adds a component as children. See Components. |
|
Adds a control as children. See Controls. |
|
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 |
---|---|---|
|
|
When set to |
|
|
The title to display |
|
|
Args to be used when indexing an i18n message. Eg: in |
|
|
An icon to be displayed before the |
|
|
|
|
|
|
|
|
The back button object. See Button |
|
|
The next button object. See Button |
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 | ||
---|---|---|---|---|
|
|
A list of column names to display. Each column name must match the recordset column name to automatically display its values.
|
||
|
|
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.
|
||
|
|
Defines the sortable columns
|
||
|
|
Defines the sorting of the recordset. It takes precedence over the
|
||
|
|
|||
|
|
Programmatically change the label of the specified columns.
|
||
|
|
Sets a transformer to a column. Each value of that column will be processed by the specified transformer (See registerTransformer())
|
||
|
|
Sets a pretty printer to a column. Each value of that column will be processed by the specified pretty printer (See registerPrettyPrinter())
|
||
|
|
Sets some pretty printer properties to a column. Each value of that column will be processed by the specified properties (See PrettyPrinterProperties)
|
||
|
|
If |
||
|
|
To define table filters:
|
||
|
|
Whether to display the row action buttons or not (Default: |
||
|
|
Whether to display the table header or not (Default: |
||
|
|
Whether to display the table footer or not (Default: |
||
|
|
Whether to display the table pagination or not (Default: |
||
|
|
Whether to render the table to host custom components on its cells or not. Enabling this feature slows down the rendering (Default: |
||
|
|
Whether to highlight the rows on mouse pointer hover (Default: |
||
|
|
Whether to set the zebra style or not (Default: |
||
|
|
Whether to display a box with an icon and a text when the table has no results (Default: |
||
|
|
The icon ti display when the table has no results. Choose one from Font Awesome. |
||
|
|
The message to display when the table has no results. |
Methods
Method | Description | ||||||
---|---|---|---|---|---|---|---|
|
Assigns a recordset to the table body (See Recordsets)
|
||||||
|
Assigns a recordset to the table footer (See Recordsets)
|
||||||
|
If set the table will paginate the results. Must be set to the total count of the records to show.
|
||||||
|
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.
|
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 |
---|---|---|
|
|
|
|
|
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: |
|
|
If set to |
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,
)
}
1 | The submitted values of the filters fields. See TableFilters |
Properties
Property | Type | Description |
---|---|---|
|
|
A |
|
|
Whether to activate the grid system or not (Default: |
|
|
Sets all the form fields readonly (Default: |
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 |
---|---|---|
|
|
The contained component |
|
|
The field label |
|
|
A list of objects to pass to the localized message (Eg. when using |
|
|
A help message |
|
|
A list of objects to pass to the localized message (Eg. when using |
|
|
Whether to display the field as nullable or not. If set will override the form |
|
|
If set to |
|
|
Defines how many columns of the grid system will be used to span the |
|
|
If the |
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')
}
Properties
Property | Type | Description |
---|---|---|
|
|
The default action |
|
|
The tail action |
|
|
The action menu |
|
|
When set to |
|
|
Set to |
|
|
If set to |
|
|
The max width in pixels that the button can reach. |
Events
Event | Description |
---|---|
|
The event is triggered on mouse click or finger tab on touch devices |
Menu
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.
Link
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',
)
}
Properties
Property | Type | Description |
---|---|---|
|
|
Icon that graphically represents the link. Choose one from Font Awesome. |
|
|
An SVG image that graphically represents the link. If specified a corresponding file must exist in the |
|
|
A label that describes the link, usually a code found in |
|
|
Point to a specific URL |
|
|
Whether to render the whole html page (or raw http body) or a Transition |
|
|
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. |
|
|
If set to |
|
|
Whether to display the content in a modal dialog or not |
|
|
When displaying the content as |
|
|
When displaying the content as |
|
|
When displaying the content as |
|
|
If set to |
|
|
Can be set to |
|
|
If specified an info message will pop up, the link will never be executed |
|
|
If specified a confirm message will pop up giving the user a chance to cancel the action |
Events
Event | Description |
---|---|
|
The event is triggered on mouse click or finger tap on touch devices |
Label
A Label
is a canvas for text and custom HTML.
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 |
---|---|---|
|
|
The text to display. If it’s a |
|
|
An html string, useful to format text or insert links |
|
|
If specified the |
|
|
An icon to display before the text, you can choose one from Font Awesome |
|
|
Determines the text horizontal alignment. It can be set to |
|
|
Determines how the text is wrapped:
|
|
|
Use a monospaced font instead of the default one |
|
|
Draws a coloured background. Useful when we want to display the label in a different color. |
|
|
If |
Separator
Wa can use separators to space between a set of fields and another one in a form.
Properties
Property | Type | Description |
---|---|---|
|
|
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 |
---|---|---|
|
|
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 |
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.
TextField
A text field.
c.form.addField(
class: TextField,
id: 'username',
icon: 'fa-user',
)
Properties
Property | Type | Description |
---|---|---|
|
|
An icon to display within the control, you can choose one from Font Awesome |
|
|
A text to display before the edit area of the control |
|
|
Max number of characters the user can input |
|
|
A text to display when the text area is empty |
|
|
Use a monospaced font instead of the default one |
|
|
Transforms the input while typing. It may be one of the following:
|
|
|
A RegEx pattern to accept only specific input (Eg. |
Methods
Method | Description |
---|---|
|
Adds an action button at the end of the control. See Link. |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
Triggered when the value changes |
Select
Displays a list of options to choose from.
Properties
Property | Type | Description |
---|---|---|
|
|
Options will be set from the recordset |
|
|
Options will be set from the List items. The key of each item will match the value of the item itself. |
|
|
Options will be set from the Enum. The key of each item will match the value of the item itself. |
|
|
Options will be set from the Map items (key/value) |
|
|
List of column names to submit as the key for the selected option (Default: |
|
|
Use the specified pretty printer to display the options. See registerPrettyPrinter(). If the registered pretty printer |
|
|
Name of the transformer to use to display the options. See registerTransformer() |
|
|
Prefix to add to each item so it can be referred in |
|
|
Whether to display the |
|
|
Displays a text when no option is selected |
|
|
If |
|
|
When there is only one available option in the list it will be automatically selected (Default: |
|
|
Enables multiple selections (Default: |
|
|
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 |
|
|
Use a monospaced font instead of the default one |
|
|
Minimum number of characters to input before the search on the server can start. Works in combination with the |
Methods
Method | Description |
---|---|
|
Returns a |
|
Returns a |
|
Returns a |
|
Returns a |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
Triggered when the value changes |
|
Triggered when |
Search on server
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 |
---|---|---|
|
|
The text to display |
Events
Event | Description |
---|---|
|
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 |
---|---|---|
|
|
Max number of characters the user can input |
|
|
Use a monospaced font instead of the default one |
Events
Event | Description |
---|---|
|
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 |
---|---|---|
|
|
How many decimal digits are allowed (Default: |
|
|
If negative values are allowed (Default: |
|
|
A list of units to select from |
|
|
The default unit to display |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
How many decimal digits are allowed (Default: |
|
|
If negative values are allowed (Default: |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
How many decimal digits are allowed (Default: |
|
|
If negative values are allowed (Default: |
|
|
Minimum number the user can input |
|
|
Maximum number the user can input |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
Minimum date the user can input |
|
|
Maximum date the user can input |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
Minimum time the user can input |
|
|
Maximum time the user can input |
|
|
The amount of minutes the user can select. For example if set to |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
Minimum date the user can input |
|
|
Maximum date the user can input |
|
|
The amount of minutes the user can select. For example if set to |
Events
Event | Description |
---|---|
|
Triggered once the content is loaded |
|
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 |
---|---|---|
|
|
The name of the controller to redirect to. If no |
|
|
The name of the action to redirect to. If no |
|
|
The params to pass when redirecting to a |
|
|
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 |
---|---|---|
|
|
|
|
|
Transformer name |
|
|
- |
|
|
Default: |
|
|
Add or change the message prefix |
|
|
Add args for the i18n message |
|
|
If |
|
|
If the value is |
|
|
If the value is 0 render the specified string instead |
|
|
For |
|
|
Change the way the date is rendered (See DateTimeFormatter) |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
For |
|
|
Whether to display Sunday as the first day of the week ( |
Websockets
TODO
Tenant Properties
TODO
System Properties
TODO
Custom CSS
TODO
Custom JavaScript
TODO