Skip to content

Latest commit

 

History

History
554 lines (450 loc) · 20.5 KB

tutorial-11.md

File metadata and controls

554 lines (450 loc) · 20.5 KB

Tutorial 11 - A Deeper Understanding of Domina Events

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.

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 tutorial-10
git checkout -b tutorial-11-step-1

Introduction

The following picture shows our old Login Form friend.

Login Form

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.

Line up Login Form with Shopping Calculator Form

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 &rarr;" 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 form action 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.

First try

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 the capture phase. In our login form example, by having used the listen! function, we have also implicitly choosen the bubbling phase. That said, in domina the submit event does not bubble up, so we needed to attach the listener function (i.e., validate-form) to the :click event of the submit button, instead of attaching it to the loginForm 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.

Prevent the default

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 for domina.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 the validation-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 the if branch traversed when its condition is true (i.e., when the email or the password are empty), it does not return the false value regularly used to block event propagation to the action attribute of the form. That's because validate-form is now internally calling prevent-default, so returning false 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 the event (i.e., :click) to make the mechanics of the listener clearer. If you want, you can can safetly unwrap validate-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

Catch early react instantly

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).

Email/Password validators

A pretty short specification of our desire could be the following:

  1. 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);

  2. 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.

Email Help

Password Help

Complete the form

Event Types

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"

In the next tutorial we're going to cover the highest and the deepest layers of the progressive enhancement strategy to the Login Form.

License

Copyright © Mimmo Cosenza, 2012-14. Released under the Eclipse Public License, the same as Clojure.