In the last tutorial we implemented a very crude server-side validator for the Login Form which exercises the same kind of syntactic rules previously implemented on the client-side.
One of our long term objectives is to eliminate any code duplication from our web applications. That's like saying that we want to stay as compliant as possible with the Don't Repeat Yourself (DRY) principle.
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-11
In this tutorial of the modern-cljs series we're going to respect the DRY principle while adhering to the progressive enhancement strategy. Specifically, we'll try to exercise both the DRY principle and the progressive enhancement strategy in one of the most relevant contexts in developing a web application: the form fields validation.
Start by writing down few technical intermediate requirements to solve this problem space. We need to:
- select a good server-side validator library
- verify its portability from CLJ to CLJS
- port the library from CLJ to CLJS
- define a set of validators to be shared between the server and the client code
- exercise the defined validators on the server and client code.
That's a lot of work to be done for a single tutorial. Take your time to follow it step by step.
If you search GitHub for a CLJ validator library you'll find quite a large number of results, but if you restrict the search to CLJS libraries only, you currently get just one result: Valip.
NOTE 1: The above assertion is no longer true. In winter 2012, when I wrote the first edition of this series of tutorials, there were almost no CLJS form validators. Today, thanks to the awesome efforts of the CLJ/CLJS community, the problem has became the opposite: there are many of them to choose from. That said, I decided to keep using the
valip
library in the series because it still has valuable lessons.
Valip has been forked from the original CLJ Valip to make it portable to the CLJS platform. This is already a good result by itself, because it demonstrates that we share our long term objective with someone else. If you then take a look at the owners of those two Github repos, you'll discover that they are two of the most prolific and active clojure-ists: Chas Emerick and James Reeves. I'm happy that the motto Smart people think alike was true.
You will eventually search for other CLJ validator libraries. For the moment, by following the Keep It Simple, Stupid (KISS) pragmatic approach, stay with the Valip library which already seems to satisfy the first three intermediate requirements we just listed in the introduction: its quality should be guaranteed by the quality of its owners and it already runs on both CLJ and CLJS.
Let's start by using valip
on the server-side first. valip
usage is
dead simple and well documented in the readme file which I
encourage you to read.
First, valip
provides you a validate
function from the valip.core
namespace. It accepts a map and one or more vectors. Each vector
consists of a key, a predicate function and an error string, like so:
(validate {:key-1 value-1 :key-2 value-2 ... :key-n value-n}
[key-1 predicate-1 error-1]
[key-2 predicate-2 error-2]
...
[key-n predicate-n error-n])
To keep things simple, we are going to apply the valip
lib to our
old Login Form
sample. Here is a possible validate
usage for the
loginForm
input elements:
(validate {:email "[email protected]" :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches *re-password*) "Invalid password format"])
As you can see, you can attach one or more predicates/functions to the
same key. If no predicate fails, nil
is returned. That's important
to remember when you'll exercise the validate
function because it
could become misleading. If at least one predicate fails, a map of
keys to error values is returned. Again, to make things easier to
understand, suppose for a moment that the email value passed to
validate
was not well formed and that the password was empty. You
would get a result like the following:
;;; a sample call
(validate {:email "zzzz" :password nil}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches *re-password*) "Invalid password format"])
;;; should return
{:email ["Invalid email format"]
:password ["Password can't be empty" "Invalid password format"]}
The value of each key in the error map is a vector, because the
validate
function can catch more than one predicate failure for
each key. That's a very nice feature to have.
Even though the valip
library already provides, via the valip.predicates
namespace, a relatively wide range of portable and pre-defined
predicates and functions returning predicates, at some point you'll need
to define new predicates by yourself. valip
makes this easy.
A predicate accepts a single argument and returns true
or
false
. Here is a sample from the valip.predicates
namespace:
(defn present?
[x]
(not (string/blank? x)))
You can also write a function which returns a predicate. The returned predicate should
accept a single argument and return a boolean value as well. Again, here is a
sample from the valip.predicates
namespace:
(defn matches
[re]
(fn [s] (boolean (re-matches re s))))
NOTE 2: I personally consider the above
matches
definition as buggy. As you'll see in a subsequent tutorial specifically dedicated tounit tests
, I always like to start testing functions from border cases. In the above returned predicate, what happens when the arguments
isnil
?You'll get a
NullPointerException
on the JVM and an almost incomprehensible error on the JSVM.A more defensive approach is to wrap the argument
s
with thestr
function:(defn matches [re] (fn [s] (boolean (re-matches re (str s)))))
Nothing new for any clojure-ist who knows about HOF, but another nice feature to have in your hands.
valip
even offers the defpredicate
macro, which allows you to
easily compose new predicates returning a boolean value. Here is a
sample from valip
itself.
(defpredicate valid-email-domain?
"Returns true if the domain of the supplied email address has a MX DNS entry."
[email]
[email-address?]
(if-let [domain (second (re-matches #".*@(.*)" email))]
(boolean (dns-lookup domain "MX"))))
If you need to take some inspiration when defining your own predicates and functions, use those samples as references.
If there was one thing that did not excite me about the original
valip library, it was its dependency on a lot of java packages in
the valip.predicates
namespace.
(ns valip.predicates
"Predicates useful for validating input strings, such as ones from a HTML
form."
(:require [clojure.string :as string]
[clj-time.format :as time-format])
(:import
[java.net URL MalformedURLException]
java.util.Hashtable
javax.naming.NamingException
javax.naming.directory.InitialDirContext
[org.apache.commons.validator.routines IntegerValidator
DoubleValidator]))
This is not a suprise if you take into account that it was made more than five years ago, when CLJS was floating around in just a few clojure-ist minds.
The surprise is that Chas Emerick chose it a couple of years later,
when CLJS had already been reified from a very smart idea into a
programming language. So, if the original valip library was so
dependent on the JVM, why would Chas Emerick choose it over other,
less-compromised CLJ validation libraries? Maybe the answer is just
that the predefined predicates and functions were confined to the
valip.predicates
namespace and most of them were easily redefinable
in portable terms.
Even if Chas Emerick made a great work by rewriting the valip
lib in
such a way that you could use it from CLJ or from CLJS, at those time
the so called
Features Expression Problem
was still to be solved in CLJ/CLJS.
You had two workarounds to use a portable (or almost portable) CLJ/CLJS lib:
- use the
:crossover
option of the thelein-cljsbuild
plugin forleiningen
, which is now deprecated; - use the lein-cljx leiningen
plugin, which added other complexity to the already complex enough
project.clj
declaration and it's now deprecated as well.
NOTE 3: in this second edition of the
modern-cljs
series of tutorials I made the choice of using theboot
building tools instead of the more standardleiningen
one. There are two main reasons for that:
- the
build.boot
is shorter/simpler than the correspondingproject.clj
when addingCLJS
stuff;- you can run everything in a single JVM instance.
Starting with the 1.7.0
release, Clojure offers a new way to solve
the above Feature Expression Problem. I'm not going to explain it
right now. I'm only mentioning it now because I rewrote the
valip
library
using the new feature in such a way that we can use it
inside this tutorial without needing the above
complexities.
As usual when using a new lib, the first thing to be done is to add it
to the dependencies
section of the project contained in the
build.boot
build file.
(set-env!
...
:dependencies '[...
[org.clojars.magomimmo/valip "0.4.0-SNAPSHOT"]
])
...
You already know. I always like to work in a live environment. So start the IFDE
cd /path/to/modern-cljs
boot dev
...
Elapsed time: 19.288 sec
and visit the Login Form.
Start the REPL
as well, but refrain yourself from starting the bREPL
on top of it, because at the moment we're going to work on the
server-side only (i.e. CLJ).
# from a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=>
Require both valip.core
and valip.predicates
namespaces from
valip
:
(use 'valip.core 'valip.predicates)
Test the validate
form we cited above:
boot.user> (validate {:email "[email protected]" :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
nil
As you see it correctly returns nil
because both the passed email
and password
strings satisfy the validation tests.
Let's see few others failing cases:
boot.user> (validate {:email nil :password nil}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Email can't be empty" "Invalid email format"], :password ["Password can't be empty" "Invalid password format"]}
There we passed a nil
value for both the email
and password
arguments and the validate
function returns a map
containing the
error massages for both the :email
and :password
keys.
boot.user> (validate {:email "[email protected]" :password nil}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:password ["Password can't be empty" "Invalid password format"]}
There we passed a well formed email
and a nil
password
arguments. The returned map
contains the :password
key/value pair
only, because the email
satisfies the validation predicates.
boot.user> (validate {:email nil :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Email can't be empty" "Invalid email format"]}
There we passed a void mail
and and a password
that matches the
used regex. So the return map
contains the :email
key only.
boot.user> (validate {:email "bademail@baddomain" :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Invalid email format"]}
There we passed a bad mail
and a valid password
arguments. The
return map
contains one message only for the bad email
address.
boot.user> (validate {:email "[email protected]" :password "badpasswd"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:password ["Invalid password format"]}
There we finally passed a nice email
and an invalid password
arguments. The return map
contains the message for the bad
password
only.
OK, we are familiarized enough with the validate
function. We can now
start coding in a source file.
To follow at our best the principle of separation of concerns, let's create a new namespace specifically dedicated to the Login Form fields validations.
To do that, create the login
subdirectory under the
src/clj/modern_cljs/
directory and then create a new file named
validators.clj
.
# from a new terminal
cd /path/to/modern-cljs
mkdir src/clj/modern_cljs/login
touch src/clj/modern_cljs/login/validators.clj
Open the newly create source file and declare the new
modern-cljs.login.validators
namespace by requiring the needed
namespaces from the valip
library we just added to the build.boot
build file.
(ns modern-cljs.login.validators
(:require [valip.core :refer [validate]]
[valip.predicates :refer [present? matches email-address?]]))
In the same file you now have to define the validators for the user
credential (i.e email
and password
).
(def ^:dynamic *re-password* #"^(?=.*\d).{4,8}$")
(defn user-credential-errors [email password]
(validate {:email email :password password}
[:email present? "Email can't be empty."]
[:email email-address? "The provided email is invalid."]
[:password present? "Password can't be empty."]
[:password (matches *re-password*) "The provided password is invalid"]))
As you see, we wrapped the above validate
call inside a function
definition.
NOTE 4: Again following separation of concerns principle, we copy the re-password regular expression from
login.clj
to here. Later we'll delete it from there.
NOTE 5:
valip
provides theemail-address?
built-in predicate which tests the user's email value using a built-in regular expression. This regular expression is based on RFC 2822 and it is defined in thevalip.predicates
namespace. So, we don't need to supply our own*re-email*
regular expression any longer. Later, we'll delete the one defined in thelogin.clj
source file.
So far, so good.
Open the login.clj
file from the src/clj/modern_cljs/
directory
and modify it as follows:
(ns modern-cljs.login
(:require [modern-cljs.login.validators :refer [user-credential-errors]]))
(defn authenticate-user [email password]
(if (boolean (user-credential-errors email password))
(str "Please complete the form.")
(str email " and " password
" passed the formal validation, but we still have to authenticate you")))
Remember to delete anything else. As soon as you save the file, everything gets recompiled.
NOTE 6: we could have returned more detailed messages from the validator result to the user. To maintain the same behaviour of the previous server-side login version we only return the "Please complete the form" message when the user typed something wrong in the form fields.
I don't know about you, but even for such a small and stupid case, the use of a validator library seems to be effective, at least in terms of code clarity and readability.
Let's now interactively verify if the just-added validator is working as expected.
Visit the login page and repeat all the interaction tests we executed in the latest tutorial. Remember to first disable the JS of the browser and eventually reload the page too.
When you submit the Login Form you should receive a Please complete the form
message anytime you do not provide the email
and/or the
password
values and anytime the provided values do not pass the
corresponding validation predicates.
When the provided values for the email and password input elements pass the validator rules, you should receive a message looking like the following: [email protected] and weak1 passed the formal validation, but we still have to authenticate you.
So far, so good.
It's now time to see if we're able to use the valip
portable library
on the client-side as well.
Before crossing the border from the server to the client,
let's take into account a very important new feature added to the
Clojure 1.7.0
release: Reader Conditionals
.
Reader Conditionals are a new capability to support portable code that can run on multiple Clojure platforms with only small changes. In particular, this feature aims to support the increasingly common case of libraries targeting both Clojure and ClojureScript.
Code intended to be common across multiple platforms should use a new supported file extension: ".cljc". When requesting to load a namespace, the platform-specific file extension (.clj, .cljs) will be checked prior to .cljc.
The updated valip
library has been rewritten using the Reader Conditionals
capability, which is the name CLJ/CLJS gave to the
solution of the Feature Expression
problem.
The modern-cljs.login.validators
namespace we just wrote is
currently hosted in the src/clj
source directory of the project. It
uses the portable namespace from the valip
lib and does not
use any features available on the JVM.
What does that mean? It means you can safely rename the file with the
cljc
extension and move it under a new src/cljc
source path
directory in the build.boot
file.
Even though boot
is so nice that it allows you to modify the environment
from the REPL with the set-env!
function, the updating of the
build.boot
file is one of the rare cases where I prefer to stop the
IFDE, make the changes and then restart it.
After having stopped IFDE:
- create the
src/cljc/modern_cljs/login
directory structure; - move there the
validators.clj
source file by renaming it asvalidators.cljc
; - update the
:source-paths
section of thebuild.boot
file by adding to its set thesrc/cljc
path; - start the IFDE again
The above steps are executed as follows:
cd /path/to/modern-cljs
mkdir -p src/cljc/modern_cljs/login
mv src/clj/modern_cljs/login/validators.clj \
src/cljc/modern_cljs/login/validators.cljc
rm -rf src/clj/modern_cljs/login
Now open the build.boot
building file to update the :source-paths
environment variable:
(set-env!
:source-paths #{"src/clj" "src/cljs" "src/cljc"}
...
)
and finally restart IFDE and the REPL as well:
cd /path/to/modern-cljs
boot dev
...
Elapsed time: 23.658 sec
# from a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=>
You're now ready to repeat our UI tests.
Visit Login page and repeat all the interaction tests we executed in the last tutorial. Remember to first disable the JS of the browser.
When you submit the Login Form you should receive a Please complete the form
message anytime you do not provide the email
and/or the
password
values and anytime the provided values do not pass the
corresponding validation predicates.
When the provided values for the email and password input elements pass the validator rules, you should receive a message looking like the following: [email protected] and weak1 passed the formal validation, but we still have to authenticate you.
Aside from the annoyance of having to repeat the interactive tests to verify that everything is still working after the above refactoring, the result is very good. But the real magic is still yet to happen.
Here we are. We've reached the point. Let's see if the magic works.
You can now start the bREPL on top of the REPL we previously started:
boot.user=> (start-repl)
...
cljs.user=>
Do you want to see if the valip
namespaces are available from the
CLJS as well? Test them by interacting with some of the predicates.
cljs.user> (require '[valip.core :refer [validate]]
'[valip.predicates :refer [present? matches email-address?]])
nil
cljs.user> (present? nil)
false
cljs.user> (present? "")
false
cljs.user> (present? "weak1")
true
cljs.user> (email-address? "[email protected]")
true
WOW, that's impressive. Let's go on by trying the validate
expressions we previously tested the server side:
cljs.user> (validate {:email "[email protected]" :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
nil
cljs.user> (validate {:email nil :password nil}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Email can't be empty" "Invalid email format"], :password ["Password can't be empty" "Invalid password format"]}
cljs.user> (validate {:email "[email protected]" :password nil}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:password ["Password can't be empty" "Invalid password format"]}
cljs.user> (validate {:email nil :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Email can't be empty" "Invalid email format"]}
cljs.user> (validate {:email "bademail@baddomain" :password "weak1"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:email ["Invalid email format"]}
cljs.user> (validate {:email "[email protected]" :password "badpasswd"}
[:email present? "Email can't be empty"]
[:email email-address? "Invalid email format"]
[:password present? "Password can't be empty"]
[:password (matches #"^(?=.*\d).{4,8}$") "Invalid password format"])
{:password ["Invalid password format"]}
Let's see if the user-credential-errors
function we
defined in the modern-cljs.login.validators
namespace is working
from the bREPL as well.
Require the namespace and then call the user-credential-errors
function:
cljs.user> (require '[modern-cljs.login.validators :refer [user-credential-errors]])
nil
cljs.user> (user-credential-errors nil nil)
{:email ["Email can't be empty." "The provided email is invalid."], :password ["Password can't be empty." "The provided password is invalid"]}
cljs.user> (user-credential-errors "bademail" nil)
{:email ["The provided email is invalid."], :password ["Password can't be empty." "The provided password is invalid"]}
cljs.user> (user-credential-errors "[email protected]" nil)
{:password ["Password can't be empty." "The provided password is invalid"]}
cljs.user> (user-credential-errors "[email protected]" "weak")
{:password ["The provided password is invalid"]}
cljs.user> (user-credential-errors "[email protected]" "weak1")
nil
WOW, I don't know about you, but anytime I see this magic at work, I could kiss the CLJ/CLJS contributors one by one. No way that anybody could convince me that there is something better than CLJ/CLJS on Planet Web.
OK, we had enough magic for now. Let's go back to Earth to update
the CLJS modern-cljs.login
namespace by using this magic.
Open the login.cljs
source file living in the src/cljs/modern_cljs
directory to start updating it while the IFDE is running:
(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]
[modern-cljs.login.validators :refer [user-credential-errors]])
(:require-macros [hiccups.core :refer [html]]))
We have updated the namespace declaration by adding the
modern-cljs.login.validators
requirement containing the
user-credential-errors
validator.
We now have to update the previously defined functions by substituting any old validation with the new one.
(defn validate-email [email]
(destroy! (by-class "email"))
(if-let [errors (:email (user-credential-errors (value email) nil))]
(do
(prepend! (by-id "loginForm") (html [:div.help.email (first errors)]))
false)
true))
The modification is limited, but very representative of the objective we
have reached. Now we are just returning any email validation error and
passing the first of them to the prepend!
function for manipulating
the DOM.
NOTE 7: Here we used the
if-let
form. If you don't understand it, I strongly suggest you to search the web for their usage. Generally speaking, the Clojure Cheatsheet, ClojureDocs, and Grimoire are good.
The next function to be reviewed is validate-password
. As you can see,
the changes are almost identical.
(defn validate-password [password]
(destroy! (by-class "password"))
(if-let [errors (:password (user-credential-errors nil (value password)))]
(do
(append! (by-id "loginForm") (html [:div.help.password (first errors)]))
false)
true))
NOTE 8: As an exercise, you can define a new function, named
validate-dom-element
, which extracts an abstraction from thevalidate-email
andvalidate-password
structure definition. It's just another application of the DRY principle. This could be the starting point of a CLJS validation library based ondefprotocol
and incorporating the CLJ/CLJS shared part of the validation: the data validation. A validation process could be seen as a two-part process: the data validation part and the user interface rendering part. By separating the concerns you can even end up with something practical for the clojure-ist community.
Finally, we need to update the validate-form
and the init
functions as well.
(defn validate-form [evt email password]
(if-let [{e-errs :email p-errs :password} (user-credential-errors (value email) (value password))]
(if (or e-errs p-errs)
(do
(destroy! (by-class "help"))
(prevent-default evt)
(append! (by-id "loginForm") (html [:div.help "Please complete the form."])))
(prevent-default evt))
true))
(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 email password)))
(listen! email :blur (fn [evt] (validate-email email)))
(listen! password :blur (fn [evt] (validate-password password))))))
NOTE 9: To maintain the same behaviour as before, we did not refactor the
validate-form
too much and just addedpassword
DOM elements tovalidate-form
itself.
Aren't you curious like me to see if everything is still working? As
soon as you save the login.cljs
everything gets recompiled and
reloaded but one thing: we modified the init
function which is
exported to JS and directly called from the script tag inside the
index.html
page. This is one of the rare cases where you need to
manually reload the page to see the result. So, just reload the
Login Form
and you'll be
launched back to the sky again.
NOTE 10: if you did not reactivate your browser JS Engine, do it before reloading the Login Form page
Repeat all the interactive tests you did. I know, it's boring, but at least you will be proud of the CLJ/CLJS community you're now a part of.
We satisfied all the five requirements we started from:
- select a good server-side validation library
- verify its portability from CLJ to CLJS
- port the library from CLJ to CLJS
- define a set of validators to be shared between the server and the client code
- exercise the defined validators on the server and client code.
Best of all, we were able to follow the DRY and the separation of concerns principles while adhering to the progressive enhancement strategy.
The only residual violation of the DRY principle regards the HTML5
validations which are still duplicated in the index.html
page. This
will be solved in successive tutorials where we will introduce the so
called pure HTML template system.
For the last paragraph of this tutorial we're going to extend what we've already done by introducing a server-side only validator which will be called via ajax by the client code.
As we said, most valip predicates are portable between CLJ and
CLJS. But not all of them. Just as an example, valip.predicates
includes the valid-email-domain?
predicate which verifies the
existence of the domain of the email address passed by the
user. Because it's implemented in terms of native java code,
valid-email-domain?
is not available on the JS platform.
It often happens that some validations are not directly executable on
the client-side. However, thanks to ajax machinery, you can bring the
result of a server-side-only validation to the client-side as
well. The valid-email-domain?
predicate is one such example. Let's
see how.
Stop the bREPL (i.e., :cljs/quit
). You're now back at the CLJ
REPL. Use the valip.predicates
namespace to familiarize yourself with
the valid-email-domain?
predicate:
boot.user> (use 'valip.predicates)
nil
boot.user> (valid-email-domain? "[email protected]")
true
boot.user> (valid-email-domain? "[email protected]")
true
boot.user> (valid-email-domain? "[email protected]")
false
Since it resides on the server-side, the valid-email-domain?
predicate
has no cross site limitations like the browser counterpart.
We now want to verify if the validate
function living in the
valip.core
namespace works in tandem with the above
valip-email-domain?
predicate:
boot.user> (use 'valip.core)
nil
boot.user> (validate {:email "[email protected]"}
[:email valid-email-domain? "The domain does not exist!"])
nil
boot.user> (validate {:email "[email protected]"}
[:email valid-email-domain? "The domain does not exist!"])
{:email ["The domain does not exist!"]}
So far, so good.
We now have to decide in which namespace to define a new validator verifying the domain of the email entered by the user.
The most obvious choice is to define such a validator in the same
namespace where we already defined the user-credential-errors
validator: the modern-cljs.login.validators
namespace.
The problem is that such a validator is definable for the JVM only
and the modern-cljs.login.validators
namespace is a portable
namespace code living in the validators.cljc
file (note the .cljc
extension).
Fortunately, as previously noted, starting from Clojure 1.7.0
we have
a pretty handy way to conditionally evaluate a form/expression
depending on the features offered by the environment at compile-time:
the Reader Conditionals.
Currently there are three available platform features identifying the environment at compile-time:
:clj
: to identify JVM:cljs
: to identify JSVM:clr
: identify the MS Common Language Runtime (i.e., MS.net
)
There are two new reader literal forms: #?
and #?@
. At the moment
we're only interested in the first one. The #?
form is interpreted
similarly to a cond
: a feature condition is tested until a match is
found, then the corresponding single expression is returned.
Note that these platform features and the corresponding
reader literals are only available in source files with .cljc
extension and you can't use them in clj
and cljs
source files.
That's enough to start using the non-portable valid-email-domain?
predicate in the portable modern-cljs.login.validators
namespace to
define a new non-portable validator by using the #?
reader literal.
It may seem complex, but it's really very easy.
Open the validators.cljc
to modify its namespace declaration and to
add a new email-domain-errors
validator which use the JVM based
valid-email-domain?
predicate.
(ns modern-cljs.login.validators
(:require [valip.core :refer [validate]]
[valip.predicates :as pred :refer [present?
matches
email-address?]]))
(def ^:dynamic *re-password* #"^(?=.*\d).{4,8}$")
(defn user-credential-errors [email password]
(validate {:email email :password password}
[:email present? "Email can't be empty."]
[:email email-address? "The provided email is invalid."]
[:password present? "Password can't be empty."]
[:password (matches *re-password*) "The provided password is invalid"]))
#? (:clj (defn email-domain-errors [email]
(validate
{:email email}
[:email pred/valid-email-domain? "The domain of the email doesn't exist."])))
There are few important notable aspects in the above code:
- The
valip.predicates
requirement form is now using both the:as
and the:refer
options. In this case the namespace declaration is shared between CLJ and CLJS and you can't refer thevalid-email-domain?
symbol on a JSVM platform. For this reason we added the:as
option; - The
email-domain-errors
validator in now wrapped into the#?
reader literal and it's going to be defined only for the JVM platform, which is identified at compile-time by the:clj
keyword; - References to the
valid-email-domain?
function must use thepred
alias; - There is not a
:cljs
condition/expression pair, meaning that the newly definedemail-domain-errors
validator is only available on the JVM platform.
We now have to call the new server-side-only validator in the
authenticate-user
function which resides in the login.clj
file.
(ns modern-cljs.login
(:require [modern-cljs.login.validators :refer [user-credential-errors
email-domain-errors]]))
(defn authenticate-user [email password]
(if (or (boolean (user-credential-errors email password))
(boolean (email-domain-errors email)))
(str "Please complete the form.")
(str email " and " password
" passed the formal validation, but we still have to authenticate you" )))
In the namespace declaration we only added the email-domain-errors
to the :refer
section of the namespace requirement.
We also changed the authenticate-user
definition by adding the new
validator inside an or
form with the old one.
NOTE 11: To maintain the previous behavior of the server-side validation we're using the validators as if they were predicates which return just
true
orfalse
.
If you now visit the Login Form you will see that if you provide a
well formed email address whose domain doesn't exist (e.g.,
[email protected]
), you'll pass the client-side validator, but
you'll fail the server-side-only validator. So far, so good. The new
server side validation works as expected. We now need to remotize the
newly defined validator for using its results on the client-side as
well.
As we already know, we can easily remotize a function by using the
shoreleave machinery. Open the remotes.clj
file and update the
namespace declaration by requiring the modern-cljs.login.validators
namespace, where we defined the email-domain-errors
server-side-only
validator. Next define the new remote function as you already did in
the 9th Tutorial with the calculate
function. Here is the
updated content of remotes.clj
(ns modern-cljs.remotes
(:require [modern-cljs.core :refer [handler]]
[compojure.handler :refer [site]]
[shoreleave.middleware.rpc :refer [defremote wrap-rpc]]
[modern-cljs.login.validators :as v]))
(defremote calculate [quantity price tax discount]
(-> (* quantity price)
(* (+ 1 (/ tax 100)))
(- discount)))
(defremote email-domain-errors [email]
(v/email-domain-errors email))
(def app
(-> (var handler)
(wrap-rpc)
(site)))
NOTE 12: We now require the namespace by using the
:as
option instead of the:refer
option because we want to use the same name for the server-side validator and its remotization.
The last step consists in calling the newly remotized function from the client-side code (i.e. CLJS).
Open the login.cljs
file from src/cljs/modern-cljs/
directory and
update its content as follows:
(ns modern-cljs.login
(:require-macros [hiccups.core :refer [html]]
[shoreleave.remotes.macros :as shore-macros])
(:require [domina.core :refer [by-id by-class value append! prepend! destroy! attr log]]
[domina.events :refer [listen! prevent-default]]
[hiccups.runtime :as hiccupsrt]
[modern-cljs.login.validators :refer [user-credential-errors]]
[shoreleave.remotes.http-rpc :refer [remote-callback]]))
(defn validate-email-domain [email]
(remote-callback :email-domain-errors
[email]
#(if %
(do
(prepend! (by-id "loginForm")
(html [:div.help.email "The email domain doesn't exist."]))
false)
true)))
(defn validate-email [email]
(destroy! (by-class "email"))
(if-let [errors (:email (user-credential-errors (value email) nil))]
(do
(prepend! (by-id "loginForm") (html [:div.help.email (first errors)]))
false)
(validate-email-domain (value email))))
After having required the shoreleave.remotes.http-rpc
namespace in the
namespace declaration, we added a new validate-email-domain
function
which wraps the remotized email-domain-errors
function via the
shoreleave remote-callback
function and manipulates the DOM of the
loginForm
with a new error/help message to the user.
We then updated the previously-defined validate-email
function by
adding the call to the newly defined validate-email-domain
function.
You should now check your work by interactively test the Login Form
again. As usual visit the index.html page and see what
happens when you provide a well-formed email address whose domain
doesn't exist (e.g. [email protected]
).
We're done. You can now stop any boot
related process and reset your
git repository.
git reset --hard
In the next Tutorial we are going to prepare the stage to
discuss unit testing. We'll also introduce the Enlive
template system to implement a server-side only version of the
Shopping Calculator aimed at adhering to the progressive enhancement
implementation strategy. We'll even see how to exercise code
refactoring to satisfy the DRY principle and to resolve a cyclic
namespaces dependency problem we'll meet along the way.
Copyright © Mimmo Cosenza, 2012-14. Released under the Eclipse Public License, the same as Clojure.