red-tape/rendering/index.html @ 15d3b832fdc5 default tip

cl-digraph: Update site.
author Steve Losh <steve@stevelosh.com>
date Wed, 21 Jun 2023 15:21:12 -0400
parents 69831b3947d7
children (none)
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Rendering / Red Tape</title>
        <link rel="stylesheet" href="../_dmedia/tango.css"/>
        <link rel="stylesheet/less" type="text/css" href="../_dmedia/style.less"/>
        <script src="../_dmedia/less.js" type="text/javascript">
        </script>
    </head>
    <body class="content">
        <div class="wrap">
            <header><h1><a href="..">Red Tape</a></h1></header>
                <div class="markdown">
<h1 id="rendering"><a href="">Rendering</a></h1><p>Red Tape does not provide any built-in functionality for rendering HTML.  It
leaves that up to you, so you can choose your preferred templating library.
However, it's built with a few key ideas in mind that should make it easier for
you do create forms that are usable and friendly for your users.</p>
<p>Let's look at a full example that shows all the things you should think about
when rendering a form.  This example will use the Hiccup templating library for
brevity, but as mentioned you can use any templating library you like.</p>
<p>Once you've read this section you're all set!  Dig in and start making some
forms.  The <a href="../reference/">reference guide</a> will be there when you need it.</p>
<div class="toc">
<ul>
<li><a href="#basic-structure">Basic Structure</a></li>
<li><a href="#rendering-a-fresh-form">Rendering a Fresh Form</a></li>
<li><a href="#rendering-errors">Rendering Errors</a></li>
</ul></div>
<h2 id="basic-structure">Basic Structure</h2>
<p>Recall the structure of the GET handler from the <a href="../basics/">basics</a> guide:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-get</span>
  <span class="p">([</span><span class="nv">request</span><span class="p">]</span>
    <span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">your-form</span><span class="p">)))</span>
  <span class="p">([</span><span class="nv">request</span> <span class="nv">form</span><span class="p">]</span>
    <span class="p">(</span><span class="nf">...</span> <span class="nv">render</span> <span class="nv">the</span> <span class="nv">page</span>, <span class="nv">passing</span> <span class="nv">in</span> <span class="nv">the</span> <span class="nv">form</span> <span class="nv">...</span><span class="p">)))</span>
</pre></div>


<p>And also recall the POST handler:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-post</span> <span class="p">[</span><span class="nv">request</span><span class="p">]</span>
  <span class="p">(</span><span class="k">let </span><span class="p">[</span><span class="nv">data</span> <span class="p">(</span><span class="ss">:params</span> <span class="nv">request</span><span class="p">)</span>
        <span class="nv">form</span> <span class="p">(</span><span class="nf">your-form</span> <span class="nv">data</span><span class="p">)]</span>
    <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="ss">:valid</span> <span class="nv">form</span><span class="p">)</span>
      <span class="p">(</span><span class="nf">...</span> <span class="k">do </span><span class="nv">whatever</span> <span class="nv">you</span> <span class="nv">need</span>, <span class="nv">then</span> <span class="nv">redirect</span> <span class="nv">somewhere</span> <span class="nv">else</span> <span class="nv">...</span><span class="p">)</span>
      <span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="nv">form</span><span class="p">))))</span>
</pre></div>


<p>As you can see, there's only one place where the form gets rendered in this
code.  This means your rendering code will all be in a single place, and should
handle result maps from fresh forms as well as ones that contain errors.</p>
<h2 id="rendering-a-fresh-form">Rendering a Fresh Form</h2>
<p>Let's talk about the fresh form first.  We'll use a simple form as an example:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">require</span> <span class="o">'</span><span class="p">[</span><span class="nv">red-tape.cleaners</span> <span class="ss">:as</span> <span class="nv">cleaners</span><span class="p">])</span>

<span class="p">(</span><span class="nf">defform</span> <span class="nv">simple-form</span> <span class="p">{</span><span class="ss">:initial</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"Steve"</span><span class="p">}}</span>
  <span class="ss">:name</span> <span class="p">[</span><span class="nv">clojure.string/trim</span>
         <span class="nv">cleaners/non-blank</span><span class="p">])</span>
</pre></div>


<p>Now we'll create the get handler for this:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="kd">defn </span><span class="nv">handle-get</span>
  <span class="p">([</span><span class="nv">request</span><span class="p">]</span>
    <span class="p">(</span><span class="nf">handle-get</span> <span class="nv">request</span> <span class="p">(</span><span class="nf">simple-form</span><span class="p">)))</span>
  <span class="p">([</span><span class="nv">request</span> <span class="nv">form</span><span class="p">]</span>
    <span class="p">(</span><span class="nf">simple-page</span> <span class="nv">form</span><span class="p">)))</span>
</pre></div>


<p>Now we'll use Hiccup to create <code>simple-page</code>:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defpage</span> <span class="nv">simple-page</span> <span class="p">[</span><span class="nv">form</span><span class="p">]</span>
  <span class="p">(</span><span class="nf">html</span>
    <span class="p">[</span><span class="ss">:body</span>
     <span class="p">[</span><span class="ss">:form</span> <span class="p">{</span><span class="ss">:method</span> <span class="s">"POST"</span><span class="p">}</span>
      <span class="p">[</span><span class="ss">:label</span> <span class="s">"What is your name?"</span><span class="p">]</span>
      <span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"name"</span>
               <span class="ss">:value</span> <span class="p">(</span><span class="nf">get-in</span> <span class="nv">form</span> <span class="p">[</span><span class="ss">:data</span> <span class="ss">:name</span><span class="p">])}]</span>
      <span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:type</span> <span class="s">"submit"</span><span class="p">}]]]))</span>
</pre></div>


<p>Notice how the <code>value</code> for the name input is pulled out of the <code>:data</code> entry of
the result map.  This ensures that the field will be filled in with the correct
initial data ("Steve" in this example).</p>
<h2 id="rendering-errors">Rendering Errors</h2>
<p>Suppose the user backspaces out the initial name and submits the form.  The
<code>non-blank</code> cleaner in the form means that a blank name is considered invalid.</p>
<p>The usual POST handler for a Red Tape form will use the GET handler to re-render
the result map of the invalid form, so our template should take this into
account and display errors properly.  Let's see how we might do that:</p>
<div class="codehilite"><pre><span class="p">(</span><span class="nf">defpage</span> <span class="nv">simple-page</span> <span class="p">[</span><span class="nv">form</span><span class="p">]</span>
  <span class="p">(</span><span class="nf">html</span>
    <span class="p">[</span><span class="ss">:body</span>
     <span class="p">(</span><span class="k">if </span><span class="p">(</span><span class="ss">:errors</span> <span class="nv">form</span><span class="p">)</span>
       <span class="p">[</span><span class="ss">:p</span> <span class="s">"There was a problem.  Please try again."</span><span class="p">])</span>
     <span class="p">[</span><span class="ss">:form</span> <span class="p">{</span><span class="ss">:method</span> <span class="s">"POST"</span><span class="p">}</span>
      <span class="p">[</span><span class="ss">:label</span> <span class="s">"What is your name?"</span><span class="p">]</span>
      <span class="p">(</span><span class="nb">when-let </span><span class="p">[</span><span class="nv">error</span> <span class="p">(</span><span class="nf">get-in</span> <span class="nv">form</span> <span class="p">[</span><span class="ss">:errors</span> <span class="ss">:name</span><span class="p">])]</span>
        <span class="p">[</span><span class="ss">:p</span> <span class="nv">error</span><span class="p">])</span>
      <span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:name</span> <span class="s">"name"</span>
               <span class="ss">:value</span> <span class="p">(</span><span class="nf">get-in</span> <span class="nv">form</span> <span class="p">[</span><span class="ss">:data</span> <span class="ss">:name</span><span class="p">])}]</span>
      <span class="p">[</span><span class="ss">:input</span> <span class="p">{</span><span class="ss">:type</span> <span class="s">"submit"</span><span class="p">}]]]))</span>
</pre></div>


<p>There are two additions here.  First we check if there were any errors at the
top of the form.  If there were, we display a message at the top of the form.</p>
<p>This is especially important when you have more than a single field, because the
field with the problem may be further down the page and the user needs to know
that they need to scroll down to see it.</p>
<p>We also check if there were any errors for the specific field, and if so we
display it.  In this simple example there's only one field, but in most forms
you'll have many fields, each of which may or may not have errors.</p>
<p>This form didn't have any form-level cleaners, but if it did we would have
wanted to display those too (probably at the top of the form).</p>
<p>Also remember that the <code>:data</code> in the form that we're using to prepopulate the
field will now contain whatever the user submitted.  That way we won't blow away
the things the user typed if there happens to be an error.  They can fix the
error and just hit submit.</p>
                </div>
            <footer><p>Made and <a href="http://sjl.bitbucket.org/d/">documented</a> with love by <a href="http://stevelosh.com">Steve
Losh</a>.</p>
<p><br/><a id="rochester-made" href="http://rochestermade.com" title="Rochester Made"><img src="http://rochestermade.com/media/images/rochester-made-dark-on-light.png" alt="Rochester Made" title="Rochester Made"/></a></p></footer>
        </div>
    </body>
</html>