Input Validation and Errors
I used to use the flash()
helper in Mojolicious for reporting errors back to the user but recently discovered I can just use Mojolicious::Validator::Validation in my templates which seems like the right tool for the job. I was already using it in my Mojolicious::Lite app like so:
$v->required('name' )->size(1, 63 );
$v->required('title')->size(1, 127 );
$v->required('post' )->size(2, 4000);
But other than changing the status to 400 I didn't realize I could use this to also report to the user that their request was invalid. The methods can be used in the templates themselves:
<form method="post">
<div class="name field">
<%= label_for name => 'Author' %>
<%= text_field name =>'Anonymous', maxlength => 63, minlength => 1 %>
<% if (my $error = validation->error('name')) { =%>
<p class="field-with-error">Invalid name: 1 to 63 characters please.</p>
<% } =%>
</div>
<div class="title field">
<%= label_for title => 'Title' %>
<%= text_field 'title', maxlength => 127, minlength => 1 %>
<% if (my $error = validation->error('title')) { =%>
<p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
<% } =%>
</div>
<div class="text field">
<%= label_for post => 'Text' %>
<%= text_area 'post', (
maxlength => 4000,
minlength => 2,
required => 'true',
rows => 6
) %>
<% if (my $error = validation->error('post')) { =%>
<p class="field-with-error">Invalid post: Up to 4,000 characters only.</p>
<% } =%>
</div>
<%= submit_button 'Post', class => 'post button' %>
</form>
So when the user makes their initial GET
request, the following HTML is rendered within the form:
<div class="name field">
<label for="name">Author</label>
<input maxlength="63" minlength="1" name="name" type="text" value="Anonymous">
</div>
<div class="title field">
<label for="title">Title</label>
<input maxlength="127" minlength="1" name="title" type="text">
</div>
<div class="text field">
<label for="post">Text</label>
<textarea maxlength="4000" minlength="2" name="post" required="true" rows="6"></textarea>
</div>
<input class="post button" type="submit" value="Post">
Now let's say you screw up and submit a null value for the Title in your subsequent POST
request, in your response the form is rendered again like this:
<div class="name field">
<label for="name">Author</label>
<input maxlength="63" minlength="1" name="name" type="text" value="anon">
</div>
<div class="title field">
<label class="field-with-error" for="title">Title</label>
<input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
<p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
</div>
<div class="text field">
<label for="post">Text</label>
<textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
</div>
<input class="post button" type="submit" value="Post">
The class
attribute field-with-error
was added to the invalid fields allowing me to decorate this to hint to the user. Then I added another little paragraph (with the same class
) to make sure accessibility isn't an issue. These are definitely scenarios where I choose TagHelpers over vanilla HTML in my templates.
Also while testing all of this, I was resorting to using curl
to submit invalid input since my browser won't let me thanks to attributes such as maxlength
and minlength
being present. Finally I realized I could do the same with Mojo's built-in commands which is awesome:
$ ./PostText.pl get -M POST -f 'name=anon' -f 'title=' -f 'post=hi' '/post'
[2022-08-15 19:28:16.84229] [76893] [trace] [jJBy5DsZGrMq] POST "/post"
[2022-08-15 19:28:16.84266] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84279] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84359] [76893] [trace] [jJBy5DsZGrMq] Rendering template "post.html.ep"
[2022-08-15 19:28:16.84564] [76893] [trace] [jJBy5DsZGrMq] Rendering template "layouts/main.html.ep"
[2022-08-15 19:28:16.84741] [76893] [trace] [jJBy5DsZGrMq] 400 Bad Request (0.005116s, 195.465/s)
<!DOCTYPE html>
<html lang="en">
<head>
<title>Post::Text - New Thread</title>
<link href="/asset/942e7be1d2/PostText.css" rel="stylesheet">
</head>
<body>
<h1>Post::Text</h1>
<nav>
<a href="/view">View</a>
<a href="/post">New</a>
</nav>
<hr>
<h2>New Thread</h2>
<form method="post">
<div class="name field">
<label for="name">Author</label>
<input maxlength="63" minlength="1" name="name" type="text" value="anon">
</div>
<div class="title field">
<label class="field-with-error" for="title">Title</label>
<input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
<p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
</div>
<div class="text field">
<label for="post">Text</label>
<textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
</div>
<input class="post button" type="submit" value="Post">
</form>
<footer>
<p>In UTF-8 we trust.</p>
</footer>
</body>
</html>
I see two ways forward on this little project. I can maybe pause now and blow this up into a full-structure Mojolicious app or I can implement my next model (involving new-to-me SQL stuff like FOREIGN KEY
and JOIN
). Really either of these will be new-to-me and will take some time so I'll probably try to pick the lowest hanging fruit of the two... Once I figure out what the hell that is.