In the previous tutorial we introduced the Ajax model of communication between the browser and the server by exploiting the shoreleave-remote-ring and shoreleave-remote libraries.
In this tutorial, prior to extending our comprehension of Ajax in the CLJS/CLJ context, we're going to get a better and deeper understanding of a few features of DOM events management provided by domina.
To fulfill this objective, we're first going to line up the login example introduced in the 4th Tutorial with the more Clojure-ish programming style already adopted for the Shopping Calculator example in the previous tutorials.
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 tutorial-10
git checkout -b tutorial-11-step-1
The following picture shows our old Login Form friend.
As you perhaps remember from the 4th Tutorial, we desire to adhere to the progressive enhancement strategy which allows any browser to access our login form, regardless of the browser capabilities.
The lowest user experience is the one offered by a web application when the browser does not support JS (or it has been disabled by the user). The highest user experience is the one offered by a web application when the the browser supports JS and the application uses the Ajax communication model.
Generally speaking, you should always start by supporting the lowest user experience. Then you step to the next layer by supporting JS and finally you realize your best user experience enhancement by introducing the Ajax model of communication between the browser and the server.
Because this series of tutorials is mostly about CLJS and not about CLJ, we skipped the layer representating the lowest user experience which is based on CLJ only. Yet, we promise to fill this gap in successive tutorials explaining the usage of CLJ libraries on the server side.
The 9th tutorial left to the smart user the task of updating the Login Form with the same kind of DOM manipulation used in implementing the Shopping Calculator.
Let's work together on the first step of this task.
We start by reviewing the html code of
login-dbg.html
.
<!doctype html>
<html lang="en">
<head>
...
...
</head>
<body>
<form action="login.php" method="post" id="loginForm" novalidate>
<fieldset>
<legend>Login</legend>
...
...
<div>
<label for="submit"></label>
<input type="submit" value="Login →" id="submit">
</div>
</fieldset>
</form>
<script src="js/modern_dbg.js"></script>
<script>
modern_cljs.login.init();
</script>
</body>
</html>
NOTE 1: The original and non-existent
login.php
server script is still attached to the formaction
attribute. In a later tutorial we're going to replace it with a corresponding service implemented in CLJ.
As you remember, when we reviewed the Shopping Calculator
code to
make it more Clojure-ish, we started by changing the type
attribute
of the Shopping form's button from type="submit"
to
type="button"
. But having decided to adhere to a progressive
enhancement strategy, this is not something that we should have done--a
plain button type is not going anywhere if the browser doesn't support JS.
So we need to stay with the submit
type of button.
Start by making the programming style of login.cljs
more
Clojure-ish. First we want to remove any CLJS/JS interop calls by using
domina. Open login.cljs
and change the init
function as follows.
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(listen! (by-id "submit") :click validate-form)))
NOTE 2: The domina.events library contains a robust event handling API that wraps the Google Closure event handling code and exposing it in an idiomatic functional way for both the
bubbling
event propagation phase and thecapture
phase. In our login form example, by having used thelisten!
function, we have also implicitly choosen thebubbling
phase. That said, in domina thesubmit
event does not bubble up, so we needed to attach the listener function (i.e.,validate-form
) to the:click
event of thesubmit
button, instead of attaching it to theloginForm
as it was before.
Now compile and run the application as usual.
lein do cljsbuild clean, cljsbuild auto dev # compile just the `dev` build
lein ring server-headless # lunch the server from a new terminal
Then visit login-dbg.html, do not fill any field (or fill
just one of them), and click the "Login" button. The application reacts by showing you the usual
alert
window reminding you to complete the form.
Click the OK
button and be prepared for an unexpected
result.
Instead of showing the login form to allow the user to complete it,
the process flows directly to the default action
attribute of the
form which, by calling a non-existent server-side script
(i.e., login.php
), returns the Page not found
message generated by
the ring/compjure server.
Take a look at the domina/events.cljs source code and direct your
attention to the Event
protocol and to the private HOF
create-listener-function
.
(defprotocol Event
(prevent-default [evt] "Prevents the default action, for example a link redirecting to a URL")
(stop-propagation [evt] "Stops event propagation")
(target [evt] "Returns the target of the event")
(current-target [evt] "Returns the object that had the listener attached")
(event-type [evt] "Returns the type of the the event")
(raw-event [evt] "Returns the original GClosure event"))
(defn- create-listener-function
[f]
(fn [evt]
(f (reify
Event
(prevent-default [_] (.preventDefault evt))
(stop-propagation [_] (.stopPropagation evt))
(target [_] (.-target evt))
(current-target [_] (.-currentTarget evt))
(event-type [_] (.-type evt))
(raw-event [_] evt)
ILookup
(-lookup [o k]
(if-let [val (aget evt k)]
val
(aget evt (name k))))
(-lookup [o k not-found] (or (-lookup o k)
not-found))))
true))
(defn listen!
([type listener] (listen! root-element type listener))
([content type listener]
(listen-internal! content type listener false false)))
Here is where you can find a beautiful Clojure-ish programming style. It
uses the anonymous reification idiom to attach predefined protocols
(i.e., Event
and ILookup
) to any data/structured data you want. Take
your time to study it. I promise you will be rewarded.
Anyway, we're not here to discuss programming elegance, but to solve the
problem of preventing the login form action
from being fired when the
user has not filled the required fields.
Thanks to the programming idiom cited above, we now know that the
Event
protocol supports, among others, the prevent-default
function, which is what we need to interupt the process of passing
control from the submit
button to the form action
attribute.
This application of the anonymous reification idiom requires that the
fired event (i.e., :click
) is passed to the validate-form
listener as
follows:
(ns modern-cljs.login
(:require [domina :refer [by-id value]]
[domina.events :refer [listen! prevent-default]]))
(defn validate-form [e]
(let [email (value (by-id "email"))
password (value (by-id "password"))]
(if (or (empty? email) (empty? password))
(do
(prevent-default e)
(js/alert "Please insert your email and password"))
true)))
NOTE 3: Remember to add the
prevent-default
symbol to the:refer
section fordomina.events
in the namespace declaration.
NOTE 4: We took advantage of the necessity to update the
validate-form
function to improve its Clojure-ish style. The semantics of thevalidation-form
are now much clearer than before:
- get the values of email and password
- if one of the two is empty, prevent the form action from being fired, raise the alert window asking the user to enter the email and the password and finally return control to the form;
- otherwise return
true
to pass control to the default action of the form.
NOTE 5: If you carefully watch the
validate-form
implementation you should note that theif
branch traversed when its condition istrue
(i.e., when thepassword
are empty), it does not return thefalse
value regularly used to block event propagation to theaction
attribute of the form. That's becausevalidate-form
is now internally callingprevent-default
, so returningfalse
would be redundant.
One last code modification in the init
function and we're done.
(defn ^:export init []
;; verify that js/document exists and that it has a getElementById
;; property
(if (and js/document
(aget js/document "getElementById"))
(listen! (by-id "submit") :click (fn [e] (validate-form e)))))
NOTE 6: Here, even if not needed, we wrapped the
validate-form
listener inside an anonymous function by passing it theevent
(i.e.,:click
) to make the mechanics of the listener clearer. If you want, you can can safetly unwrapvalidate-form
.
As usual, let's verify our work by visiting the login-dbg.html page. If you have stopped the previous application run, execute the usual commands again from the terminal prior to visiting the login page.
lein do cljsbuild auto dev, lein ring server-headless
It's now time to see if we can improve the user experience of the login form by introducing few more DOM events and DOM manipulation features of Domina.
One of the first lessons I learned when I started programming was that any error has to be caught and managed as soon as possible.
In our login form context, as soon as possible means that the syntactical correctness of the email and password typed in by the user has to be verified as soon as their input fields lose focus (i.e., blur).
A pretty short specification of our desire could be the following:
-
As soon as the email input field loses focus, check its syntactical correctness by matching its value against one of the several email regex validators available on the net; if the validation does not pass, make the error evident to help the user (e.g., by showing a red message, refocusing the email input field and making its border red);
-
A soon as the password input field loses focus, check its syntactical correctness by matching its value against one of the several password regex validators; if the validation does not pass, make the error evident to the user.
Although a nice looking implementation of the above specification is left to you, let's show at least a very crude sample from which to start.
NOTE 7: Take a look at the end of this post for an HTML5-compliant approach to password validation.
Open the login.cljs
source file and start by adding two
dynamic vars to be used for the email
and password
fields
validation:
;;; 4 to 8, at least one numeric digit.
(def ^:dynamic *password-re* #"^(?=.*\d).{4,8}$")
(def ^:dynamic *email-re* #"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")
Now add the :blur
event listener to both the email
and password
input fields in the init
function:
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(let [email (by-id "email")
password (by-id "password")]
(listen! (by-id "submit") :click (fn [evt] (validate-form evt)))
(listen! email :blur (fn [evt] (validate-email email)))
(listen! password :blur (fn [evt] (validate-password password))))))
We have not passed the event to the two new listeners because, as opposed
to the previous validate-form
case, it is not needed to
prevent any default action or to stop the propagation of the
event. Instead, we passed them the element on which the blur event
occurred.
Now define the validators. Here is a very crude implementation of them.
(defn validate-email [email]
(destroy! (by-class "email"))
(if (not (re-matches *email-re* (value email)))
(do
(prepend! (by-id "loginForm") (html [:div.help.email "Wrong email"]))
false)
true))
(defn validate-password [password]
(destroy! (by-class "password"))
(if (not (re-matches *password-re* (value password)))
(do
(append! (by-id "loginForm") (html [:div.help.password "Wrong password"]))
false)
true))
I'm very bad both in HTML and CSS. So, don't take this as something to
be proud of. Anyone can do better than me. I just added a few CSS classes
(i.e., help
, email
and password
) using the hiccups library to
manage the email and password help messages.
To complete the coding, review the validate-form
function as follows:
(defn validate-form [evt]
(let [email (by-id "email")
password (by-id "password")
email-val (value email)
password-val (value password)]
(if (or (empty? email-val) (empty? password-val))
(do
(destroy! (by-class "help"))
(prevent-default evt)
(append! (by-id "loginForm") (html [:div.help "Please complete the form"])))
(if (and (validate-email email)
(validate-password password))
true
(prevent-default evt)))))
Note that now the validate-form
internally calls the two newly-defined
validators and if they do not both return true
, it calls
prevent-default
to prevent the action
attached to
the loginForm
from being fired.
Following is the complete login.cljs
source code containing the
updated namespace declaration and referencing the
hiccups library.
(ns modern-cljs.login
(:require-macros [hiccups.core :refer [html]])
(:require [domina :refer [by-id by-class value append! prepend! destroy!]]
[domina.events :refer [listen! prevent-default]]
[hiccups.runtime]))
(def ^:dynamic *password-re* #"^(?=.*\d).{4,8}$")
(def ^:dynamic *email-re* #"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")
(defn validate-email [email]
(destroy! (by-class "email"))
(if (not (re-matches *email-re* (value email)))
(do
(prepend! (by-id "loginForm") (html [:div.help.email "Wrong email"]))
false)
true))
(defn validate-password [password]
(destroy! (by-class "password"))
(if (not (re-matches *password-re* (value password)))
(do
(append! (by-id "loginForm") (html [:div.help.password "Wrong password"]))
false)
true))
(defn validate-form [evt]
(let [email (by-id "email")
password (by-id "password")
email-val (value email)
password-val (value password)]
(if (or (empty? email-val) (empty? password-val))
(do
(destroy! (by-class "help"))
(prevent-default evt)
(append! (by-id "loginForm") (html [:div.help "Please complete the form"])))
(if (and (validate-email email)
(validate-password password))
true
(prevent-default evt)))))
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(let [email (by-id "email")
password (by-id "password")]
(listen! (by-id "submit") :click (fn [evt] (validate-form evt)))
(listen! email :blur (fn [evt] (validate-email email)))
(listen! password :blur (fn [evt] (validate-password password))))))
To make the help messages more evident to the user, add the following CSS rule
to styles.css
which resides in the resources/public/css
directory.
.help { color: red; }
Now compile and run the application as usual:
lein cljsbuild auto dev
lein ring server-headless # in a new terminal
Then visit login-dbg.html to verify the result by playing with the input fields and the login button. You should see something like the following pictures.
If you're interested in knowing all the event types supported by Domina, here is the native code goog.events.eventtype.js which enumerates all events supported by the Google Closure native code on which Domina is based.
Another way to know which events are supported by Domina is to run brepl
and inspect the Google library goog.events/EventType
directly. If we do
this in the domina.events
namespace it will save some typing.
Running ClojureScript REPL, listening on port 9000.
"Type: " :cljs/quit " to quit"
ClojureScript:cljs.user> (in-ns 'domina.events)
ClojureScript:domina.events> (map keyword (gobj/getValues events/EventType))
(:click :dblclick :mousedown :mouseup :mouseover :mouseout :mousemove
:selectstart :keypress :keydown :keyup :blur :focus :deactivate :DOMFocusIn
:DOMFocusOut :change :select :submit :input :propertychange :dragstart :drag
:dragenter :dragover :dragleave :drop :dragend :touchstart :touchmove
:touchend :touchcancel :beforeunload :contextmenu :error :help :load
:losecapture :readystatechange :resize :scroll :unload :hashchange :pagehide
:pageshow :popstate :copy :paste :cut :beforecopy :beforecut :beforepaste
:online :offline :message :connect :webkitTransitionEnd :MSGestureChange
:MSGestureEnd :MSGestureHold :MSGestureStart :MSGestureTap
:MSGotPointerCapture :MSInertiaStart :MSLostPointerCapture :MSPointerCancel
:MSPointerDown :MSPointerMove :MSPointerOver :MSPointerOut :MSPointerUp
:textinput :compositionstart :compositionupdate :compositionend)
ClojureScript:domina.events>
Remember to visit login-dbg.html to activate the brepl before evaluating any expression in the brepl.
To complete the application of the progressive enhancement strategy to the login form, in future tutorials we'll introduce friend and line up the login form to the shopping form approach adopted in the 10th tutorial to allow the browser to communicate with the server via Ajax.
If you created a new git branch as suggested in the preamble of this tutorial, I suggest you to commit the changes as follows
git commit -am "deeper understanding"
Next Step - Tutorial 12: HTML on Top, Clojure on the Bottom
In the next tutorial we're going to cover the highest and the deepest layers of the progressive enhancement strategy to the Login Form.
Copyright © Mimmo Cosenza, 2012-14. Released under the Eclipse Public License, the same as Clojure.