or: How I Learned to Stop Worrying and Love the WWW

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.

#perl
#mojolicious