Before I start work on my more substantial web project, I wanted to explore validation of user input on my little test project. It's proving to be more difficult than I had hoped.
Step one is to add a simple validate()
method to my user class. The user's age must not be negative:
fun validate(): Map<String, String> {
val errorMap = mutableMapOf<String, String>()
if (age < 0) {
errorMap.put("age", "Age must be greater than zero")
}
return errorMap
}
The validate method will return a map of fields and their respective error messages. The UserController's add-submit
needs updating to call the validation, and to put the errorMap
on to the session if there are errors.
post("/add-submit") {
val u: User = User(request.queryParams("name"), request.queryParams("age").toInt())
val errorMap = u.validate()
if (!errorMap.isEmpty()) {
request.session().attribute("errors", errorMap)
redirect(controllerHome)
} else {
logger.info("Submitting user information for user ${u}")
userService.addUser(u)
redirect(controllerHome)
}
}
Now, in SpringMVC or Grails I could rely on a 'flash', a session attribute that will only exist for the next request. When errors are found and the controller redirects to controllerHome
(which contains the add user form), this errorMap
would exist on the session and available to be displayed. Subsequent requests would clear the session attribute, so if the user navigated away, or resubmitted with correct data, the errors would be gone. SparkJava and kotlin-spark do not have a built-in flash component, so I've been trying to create one myself.
After a bit googling, I found an example written in Groovy but I really struggled to understand or recreate it in Kotlin. In particular, in SparkJava there are two requests made when a form is submitted (I guess the POST request and a subsequent GET), so I can't clear my flash session attribute until after the second request is made. Keeping track of this was really frustrating, especially as I wanted to make my flash generic enough to handle multiple different flash attributes at once (imagine a validation error message, a pop-up information box, a session time-out warning, etc).
I've got something working by creating a second session attribute (called errorCount
which is created in the AbstractController
's before { ... }
block. The errorMap<
is also put on to the model at this stage. The errorCount
is incremented with every request. In the after { ... }
block, if the errorCount
is greater than 1, it and the errorMap
is removed from the session attribute.
This works, but it isn't a true 'flash' and it's not generic enough. More work to be done here.
Displaying Validation Error Messages
At this stage, if there has been a validation error, the errorMap
should exist on the view model, and can be displayed in the Thymeleaf template. Something like this:
<div th:if="${errorMap != null}">
<p>Errors found</p>
<ul th:each="error : ${errorMap}">
<li>Field: <span th:text="${error.key}"></span>, error is: <span th:text="${error.value}"></span>
</li>
</ul>
</div>
This displays each of the errors in turn. I'd much rather display the errors attached to the relevant field, a bit like this:
<div class="input-field col s2">
<label for="addUser-age">Age</label>
<input id="addUser-age" name="age" placeholder="Age" class="input-field col s3" type="number"
th:value="${age}">
<div th:if="${errorMap != null}">
<p th:if="${errorMap.get('age')}" th:text="${errorMap.get('age')}">Validation error message for age</p>
</div>
</div>
It's not pretty. There's quite a lot of boilerplate code here. I wish Thymeleaf
was more null-tolerant - for instance, it would be nice if it would fail silently if errorMap
is null and just not display anything. As it is, I'm having to wrap everything in th:if=${errorMap != null}"
checks. I believe that Spring adds a th:error="..."
element to Thymeleaf to handle this much better.
So basic validation and error messages are working. But I'm not happy with the solution so far, and I'm going to do more on this before I'm ready to move on to my main project.
Domain name
I need to find a suitable domain name for my big project. But I haven't even got a name for the project yet :). In the meantime, through a bit of DNS jiggery-pokery, you can now access my Heroku-hosted test application via this blog at... link expired.