Tutorial 11 - HTML on Top, Clojure on the Bottom
November 1, 2016 · View on GitHub
In the previous tutorial, we used DOM events to further the progressive enhancement strategy of our Clojurean web application. By implementing (via CLJS) the JavaScript layer in the previous tutorial, we only covered one of the four typical layers of that strategy. We still need to implement the topmost layer, the HTML5 layer, and the two lowest ones: the Ajax layer and the pure server-side layer.
Preamble
If you want to start working from the end of the previous tutorial, assuming you've git installed, do as follows.
git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout se-tutorial-10
Introduction
In this tutorial we're going to cover the highest and lowest layers
of the login form example we started to cover in the
previous tutorial. We leave the details of the highest layer
(i.e., HTML5) to anyone who wants to play with it. Here we only want to
introduce the new title and pattern HTML5 attributes, which we will
use by pairing the corresponding patterns and help messages
previously used by the validate-email and validate-password
functions.
Launch IFDE
Launch IFDE as usual
boot dev
...
Elapsed time: 20.296 sec
Visit localhost:3000/index.html and
then run the bREPL
boot repl -c
...
boot.user> (start-repl)
...
cljs.user>
We are now ready for the first step.
The highest surface
Here is the relevant fragment of the updated index.html introducing
the above HTML5 new features.
<html>
...
<form action="login.php" method="post" id="loginForm">
...
<div>
<label for="email">Email Address</label>
<input type="email" name="email" id="email"
placeholder="email"
title="Type a well-formed email!"
pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
required>
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" id="password"
placeholder="password"
title="Password is from 4 to 8 chars with 1 number!"
pattern="^(?=.*\d).{4,8}$"
required>
</div>
...
</html>
As you can see, we removed the novalidate HTML5 attribute from the
form and added the placeholder, title and pattern HTML5
attributes to both the email and the password HTML5 input field
elements of the form.
As usual, as you save the file, IFDE will reload it.
The pattern values (i.e. regexp) are a kind of code duplication I
really hate. Luckily we have a handy solution based on the
domina library. Let's modify the validate-email and
validate-password functions in such a way that, instead of having
those patterns and help messages defined in the source code, we can
get them from the input elements attributes by using the attr
function defined in the domina.core namespace.
bREPLing with HTML5 new attributes
Go the bREPL and familiarize with the new HTML5 attributes.
First require the domina.core namespace, then ask for the attr
docstring and finally get the value of the pattern and title
attributes:
cljs.user> (require '[domina.core :as dom])
nil
cljs.user> (doc dom/attr)
-------------------------
domina.core/attr
([content name])
Gets the value of an HTML attribute. Assumes content will be a single node. Name may be a stirng or keyword. Returns nil if there is no value set for the style.
nil
cljs.user> (dom/attr (dom/by-id "email") :pattern)
"^[_a-z0-9-]+(\\.[_a-z0-9-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,4})$"
cljs.user> (dom/attr (dom/by-id "email") :title)
"Type a well-formed email!"
We are now ready to substitute the previous validate-email and
validate-password functions with the ones getting the regexp and the
help message directly from the Login Form.
login.cljs
Open the login.cljs file and edit it as follows:
(ns modern-cljs.login
(:require [domina.core :refer [append!
by-class
by-id
destroy!
prepend!
value
attr]]
[domina.events :refer [listen! prevent-default]]
[hiccups.runtime])
(:require-macros [hiccups.core :refer [html]]))
(defn validate-email [email]
(destroy! (by-class "email"))
(if (not (re-matches (re-pattern (attr email :pattern)) (value email)))
(do
(prepend! (by-id "loginForm") (html [:div.help.email (attr email :title)]))
false)
true))
(defn validate-password [password]
(destroy! (by-class "password"))
(if (not (re-matches (re-pattern (attr password :pattern)) (value password)))
(do
(append! (by-id "loginForm") (html [:div.help.password (attr password :title)]))
false)
true))
NOTE 1: we added the
attrsymbol to the:refersection of thedomina.corerequirement, and we then deleted the previous definitions of both*email-re*and*password-re*.
As usual, check that the Login Form is still working as expected.
HTML5 Validation
Now disable JavaScript in your browser, reload the page and click
the Login button without having filled the email and the password
fields.
If you're using an HTML5 browser you should see a message telling you to
fill out the email field and the text specified as the value of the
title attribute previously set up in the form.
If you don't use an HTML5 browser, the new attributes are just ignored
and the process continues by calling the still non-existent
login.php server-side script, resulting in the usual Not found page
from the compojure routes we defined in the 9th Tutorial.
You can simulate this last effect with an HTML5 compliant browser by
re-adding the novalidate attribute to loginForm.
It's now time to take care of this error by implementing the server-side login service, which represents the deepest layer of the progressive enhancement strategy -- the one we should have started from in a real web application development when we want to follow the progressive enhancement strategy.
The deepest layer
As you already know, this series of tutorials is about CLJS, not CLJ. On the net there are already some very good tutorials on CLJ web application development using ring and compojure. Despite this, we should at least provide a starting point and a direction to follow.
index.html
First of all, we need to delete the login.php value originally
attached to the action attribute of the loginForm and substitute it
with a call to a compojure route. We'll call this new route
login. Open the index.html and do the change
<!doctype html>
<html lang="en">
...
<form action="login" method="post" id="loginForm">
...
</form>
...
</html>
Now, after any HTML5 field validation has succeeded, the browser is
going to request the /login resource from the server using the
POST method, passing the email and password values
provided by the user.
core.clj
We should now take a look at core.clj where the ring request handler
and the compojure routes have been defined. The received POST /login
request traverses the various ring middleware layers to finally reach
handler which has to handle the request and provide a response to
be returned to the browser.
At the moment, handler doesn't yet know how to manage a POST
method request and we need to add it to handler.
Following is the updated core.clj source code.
(ns modern-cljs.core
(:require [compojure.core :refer [defroutes GET POST]] ; <- add POST
[compojure.route :refer [not-found files resources]]))
(declare authenticate-user)
(defroutes handler
(GET "/" [] "Hello from Compojure!") ;; for testing only
(files "/" {:root "target"}) ;; to serve static resources
(POST "/login" [email password] (authenticate-user email password)) ; <- add POST route
(resources "/" {:root "target"}) ;; to serve anything else
(not-found "Page Not Found")) ;; page not found
Whenever the web server receives a POST request with the "/login"
URI, the email and password parameters are extracted from the body
of the POST request and the authenticate-user function will be called
with those parameters.
We have not yet defined the authenticate-user function. In order
to respect the separation of concerns principle, we'll define it
later inside a new modern-cljs.login namespace which is going to be
declared in the corresponding login.clj Clojure source file.
Keep in mind that we're now working on the server-side, which means we're coding with CLJ, not CLJS.
login.clj
To keep things simple, at the moment we're not going to be concerned with user authentication or with any security issues. We also don't want to worry about returning a nice HTML response page. We just want to see if the working parts have been correctly set up.
Create the file login.clj in the src/clj/modern_cljs/ directory and
edit it as follow:
(ns modern-cljs.login)
(def ^:dynamic *re-email*
#"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")
(def ^:dynamic *re-password*
#"^(?=.*\d).{4,8}$")
(defn- validate-email [email]
(if (re-matches *re-email* email)
true))
(defn- validate-password [password]
(if (re-matches *re-password* password)
true))
(defn authenticate-user [email password]
(if (or (empty? email) (empty? password))
(str "Please complete the form")
(if (and (validate-email email)
(validate-password password))
(str email " and " password
" passed the formal validation, but you still have to be authenticated"))))
NOTE 2: both
validate-emailandvalidate-passwordhave been defined to be private to themodern-cljs.loginnamespace because they are only used internally by theauthenticate-userfunction.
There are a lot of bad things in the above code. The most obvious is the code duplication between the client side and the server side code. For the moment, be forgiving and go on by verifying this code works.
But first go back to core.clj CLJ source file, update the namespace
declaration by adding the newly modern-cljs.login namespace
requirement and remove the authenticate-user declaration because
it's now defined in that namespace.
Following is the updated core.clj source code.
(ns modern-cljs.core
(:require [compojure.core :refer [defroutes GET POST]]
[compojure.route :refer [not-found files resources]]
[modern-cljs.login :refer [authenticate-user]]))
(defroutes handler
(GET "/" [] "Hello from Compojure!") ;; for testing only
(files "/" {:root "target"}) ;; to serve static resources
(POST "/login" [email password] (authenticate-user email password))
(resources "/" {:root "target"}) ;; to serve anything else
(not-found "Page Not Found")) ;; page not found
First try
Disable JavaScript in the browser and remember that we removed the
novalidate attribute flag from the loginForm.
Click the Login button without having filled any fields.
If you're using an HTML5 browser, it will ask you to fill
in the email address field with a well-formed email. Do it and click again
on the Login button. The browser will now ask you to fill in the password
field by typing a password with length between 4 and 8 digits and with at
least 1 numeric digit. Do it and click the Login button again.
If everything has gone well, the browser should now show you a message
saying something like this: you@yourdomain.com and weak1 passed the
formal validation, but you still have to be authenticated`.
Great. We got what we expected.
Now add again the novalidate attribute flag to the loginForm in
the index.html source code, reload the page and click the Login
button without having filled any field. The browser should now show
you the Please complete the form message.
Great. We got what we expected.
Now re-enable JavaScript, go back to the index.html page and
click the Login button again. You should see the Please complete
the form message shown at the bottom of the Login Form. Try to type
an invalid email address and click the button again. You should now
see the Type a well-formed email! shown at the top of the login
form.
Great. We again got what we expected.
We still have a lot of work to do, but we were able to implement 3 of the 4 layers of the progressive enhancement approach for web application development. And we used Clojure on the server-side and ClojureScript on the client-side. Not bad so far.
We already know from the Shopping Calculator example (9th Tutorial) how to implement the Ajax layer. Before returning to that, the next few tutorials will introduce us to a couple of entirely new topics.
You can now stop any boot related process and reset your git repository.
git reset --hard
Next Step - Tutorial 12: Don't Repeat Yourself
One of our long-term objectives is to eliminate any code duplication from our web applications. That's like saying we want to comply with the Don't Repeat Yourself (DRY) principle. In the next tutorial we're going to exercise the DRY principle while adding a validator library to the project.
License
Copyright © Mimmo Cosenza, 2012-14. Released under the Eclipse Public License, the same as Clojure.