Dueuno Elements #2 — Let’s be CRUD(e) for once

Gianluca Sartori
5 min readJun 26, 2024

--

DISCLAIMER: I LOVE BEING CRUDE. DON’T JUDGE ME. IT’S JUST A GAME.

Okay, we have an empty box now. Let’s fill it with some fruits (fruit is good for our health).

Features

An application is a set of Features. A Feature is made of a Menu Item — so that our users can access it — and a Content, a bidimensional area we can fill with buttons and stuff.

The dark area on the left is the Main Menu. The light area is where the Content is rendered.

We create features in the application initialization method.

Edit ~/demo/grails-app/init/BootStrap.groovy

package com.example

import dueuno.elements.core.ApplicationService

class BootStrap {

ApplicationService applicationService

def init = { servletContext ->
applicationService.init {

registerFeature(
controller: 'person',
icon: 'fa-user',
favourite: true,
)

}
}

def destroy = {
}
}

The object in charge of creating a feature, and other application-wide stuff, is the ApplicationService.

To register a Feature we need to specify the name of the controller that implemets the feature. Eg. in our case, the feature will be available at the following URL:

http://localhost:8080/person

We can couple it with an optional icon, from the free set provided by Font Awesome, to decorate the Menu Item.

Last but not least, we can register the feature as a favourite one. Favourite features are immediatly available clicking the Home buttom (top-left).

Execute ./gradlew bootRun

Click on the feature to get a (404) Not Found! message. That’s fine, we still haven’t implemented anything.

Controllers and Actions

Look at the URL. It says:

http://localhost:8080/person/index

We have three things here:

  1. http://localhost:8080
    This is the address and port where we can find our application.
  2. person
    This is the Controller name. A Controller is just a container. It contains Actions.
  3. index
    This is an Action name. An Action is a piece of code that implements some logic.

Even if a Feature is accessed from a specific URL (controller/action pair), a Feature may be composed by more than one Controller and many, many… many Actions.

  • Controllers are Groovy classes located under the folder: ~/demo/grails-app/controllers/
  • Actions are methods of a Controller class.

Structure

It’s time to feed our newborn creature. We are going to implement a CRUD view, writing the PersonController, with different actions in order to display a list of people and give the user the ability to create, edit and delete one or more persons.

We create a skeleton first, a controller with just the action firms, so we can focus on the structure.

Create a new file ~/demo/grails-app/controllers/com/example/PersonController.groovy

package com.example

import dueuno.elements.core.ElementsController
import grails.validation.Validateable

class PersonController implements ElementsController {

def index() {
// Displays a list of people
}

def create() {
// Displays a form to input person data
}

def onCreate(PersonValidator val) {
// Creates a new person
}

def edit() {
// Displays a form to edit person data
}

def onEdit(PersonValidator val) {
// Updates a person record
}

def onDelete() {
// Deletes a person
}
}

class PersonValidator implements Validateable {
// Will validate user input
}

Please focus your attention to:

  1. The controller class name must be suffixed by the word Controller. That’s why our person controller is called PersonController (this is a convention of the Grails Framework).
  2. The person controller implements ElementsController. This makes the Dueuno Elements API available to our actions (NOTE: If you use IntelliJ IDEA Ultimate with the Grails plugin you can avoid implementing ElementsController and everything will magically work as expected. Yay!).
  3. We use a convention to name the actions. When they start with the on prefix, they execute some logic in the background. When they don’t, they render a user interface. We are also using a naming standard here, we may change the action names, but for now let’s not add too much complications.

It’s time to be CRUD

Edit ~/demo/grails-app/controllers/com/example/PersonController.groovy with the code below

package com.example

import dueuno.elements.contents.*
import dueuno.elements.controls.*
import dueuno.elements.core.ElementsController
import grails.validation.Validateable

import java.time.LocalDate

class PersonController implements ElementsController {

static final List personRegistry = [
[id: 1, firstname: 'Gianluca', lastname: 'Sartori', birthdate: LocalDate.of(1979, 6, 24)],
[id: 2, firstname: 'John Luke', lastname: 'Taylor', birthdate: LocalDate.of(1921, 6, 24)],
[id: 3, firstname: 'Juan Lucas', lastname: 'Sastre', birthdate: LocalDate.of(1942, 6, 24)],
]

def index() {
def c = createContent(ContentList)
c.table.with {
columns = [
'firstname',
'lastname',
'birthdate',
]
}

c.table.body = personRegistry
c.table.paginate = personRegistry.size()

display content: c
}

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

c.form.with {
validate = PersonValidator
addField(
class: TextField,
id: 'firstname',
)
addField(
class: TextField,
id: 'lastname',
)
addField(
class: DateField,
id: 'birthdate',
)
}

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

return c
}

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

def onCreate(PersonValidator val) {
if (val.hasErrors()) {
display errors: val
return
}

def last = personRegistry.max { it.id }
personRegistry << [
id: last ? last.id + 1 : 1,
firstname: params.firstname,
lastname: params.lastname,
birthdate: params.birthdate,
]

display action: 'index'
}

def edit() {
def obj = personRegistry.find { it.id == params.id }
def c = buildForm(obj)
display content: c, modal: true
}

def onEdit(PersonValidator val) {
if (val.hasErrors()) {
display errors: val
return
}

def obj = personRegistry.find { it.id == params.id }
obj.firstname = params.firstname
obj.lastname = params.lastname
obj.birthdate = params.birthdate

display action: 'index'
}

def onDelete() {
try {
personRegistry.removeIf { it.id == params.id }
display action: 'index'

} catch (e) {
display exception: e
}
}
}

class PersonValidator implements Validateable {
String firstname
String lastname
LocalDate birthdate
}

Execute ./gradlew bootRun

Contents

There’s a lot of stuff here. The most important things now are:

  1. Contents. A Content is the canvas on which we design the UI. To do it we add Components and Controls. You can’t see it in the example because we are using preconfigured contents for tables (ContentList) and forms (ContentCreate & ContentEdit)
  2. The display() method. Each action terminates its execution with the display() method. This is the way we display the UI or route from one action to the other.

ATTENTION PLEASE: For the sake of the article I’ve implemented the Business Logic within the controller class. This is not somehting we do. Don’t do it. Don’t. Ever.

Don’t.

Conclusions

In the next article we are going to see how and where to implement the Business Logic adding a database to this Supa-Dupa-Cool-And-Fool application.

👉 Read the next article!

👍 Subscribe

In the next-next articles we are going to deepen our intimacy with Contents, Components and Controls. Subscribe and stay tuned.

--

--

Gianluca Sartori

Author of Dueuno Elements (dueuno.org). Write backoffice web applications with one single programming language: Apache Groovy.