In this tutorial we're going to introduce Domina to improve the idiomaticity of the login form validation we presented in the previous tutorial.
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-04
In the previous tutorial we started coding in CLJS by directly translating from JS to CLJS by using the JS interop features of CLJS. Now it's time to try something better.
Domina is a jQuery inspired DOM manipulation library for ClojureScript. It provides a functional, idiomatic Clojure interface to the DOM manipulation facilities provided by the Google Closure library... While Domina does not provide any innovations, it attempts to provide a basic functional interface to DOM manipulation that feels natural in ClojureScript.
When I first met clojurescriptone
in search for a CLJS guide,
and read about Design and templating
, I found myself very much in
agreement with the following reasoning:
Many Clojure web applications use Hiccup for HTML templating. If the programmer is also the designer, then Hiccup is ideal. However, most developers are bad at design. We need to work with people who are good at design and who don't need to care about Clojure. ClojureScript One proposes one approach to templating which allows designers to work with HTML, CSS and images without having to set an eye on Hiccup data structures or those pesky parentheses.
NOTE 1: both
clojurescriptone
repo and the correspondingDesign and templating
documentation have been removed and are no longer available.
Our old index.html
friend is going to be our pure HTML/CSS template
and domina
is going to be our CLJS library to interface the DOM of
the page in more idiomatic CLJS.
Nowadays, the diffusion of the so called Single Page Application (SPA)
is progressively changing the web development landscape and it's not
unusual to see a single HTML page composed of one div
tag only
delegating all the DOM manipulation to JS.
That said, there is still a very large number of organizations keeping the HTML designers separated from the developers and we should be ready to give life to a bunch of pure static HTML/CSS pages.
Domina was one of the first DOM libraries written in CLJS and it
has not been updated to follow the evolution of CLJS compiler. If you
use the canonical domina
release (i.e., [domina "1.0.3"]
), during
the compilation you'll get a warning about the fact that it uses a
single segment namespace. Even if those warnings do not affect the
behavior of the lib in the context of this tutorial, I really hate
warnings. So I prepared a non canonical domina
release which fixes
those warnings. That said, even if I would never suggest to use
domina
in a new CLJS project, this tutorial could still be useful to
understand the way CLJS works.
As usual to use a new library, you need to add it to the dependencies
section of the build.boot
file living in the home directory of the
project:
(set-env!
...
:dependencies '[
...
[org.clojars.magomimmo/domina "2.0.0-SNAPSHOT"]
])
As we learnt in the previous tutorial, the use of the IFDE allows us to familiarize ourselves with the CLJS language by evaluating expressions in the bREPL before extending an application by directly coding into the source files.
So, let's start the IFDE as usual:
boot dev
Retrieving domina-2.0.0-20151125.115321-1.jar from https://clojars.org/repo/
...
Compiling ClojureScript...
• js/main.js
Elapsed time: 22.633 sec
As usual, after the boot dev
command finishes with the CLJ
compilation, visit the http://localhost:3000
URL and then launch the
bREPL in a new terminal from the project home directory:
# from a new terminal
cd /path/to/modern-cljs
boot repl -c
...
boot.user=> (start-repl)
...
cljs.user=>
Before we start bREPLing, let's review the content of the
html/index.html
file containing the login form we used in
Tutorial 4:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Login</title>
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- login.html -->
<form action="login.php" method="post" id="loginForm" novalidate>
<fieldset>
<legend>Login</legend>
<div>
<label for="email">Email Address</label>
<input type="email" name="email" id="email" required>
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
</div>
<div>
<label for="submit"></label>
<input type="submit" value="Login →" id="submit">
</div>
</fieldset>
</form>
<script src="js/main.js"></script>
</body>
</html>
Domina offers several selector functions: xpath
, in the
domina.xpath
namespace, and sel
, in the domina.css
namespace. But it also features the by-id
, value
and set-value!
functions defined in the domina.core
namespace, which is the one
we're going to use.
The nice thing about by-id
and id
, inherited from the underlying
Google Closure Library (GCL) on top of which domina
is implemented,
is that they take care of verifying if the passed argument is a
string. As we anticipated, the domina.core
namespace offers other
useful functions we're going to use: value
, which returns the value
of the passed element, and set-value!
which sets its value.
NOTE 2: as we already saw, when a function modifies an argument passed to it, by Clojure naming convention a bang "!" is added at the end of the function name.
NOTE 3: Due to a bug of the
boot-cljs-repl
task, we need to first require a namespace from a namespace declaration to be able to require it in the bREPL as well. We will usedomina
later to update ourvalidate-form
function inlogin.cljs
. You can require thedomina.core
namespace there.
Change the first line of your login.cljs
file:
(ns modern-cljs.login
(:require [domina.core :refer [by-id value set-value!]]))
Let's now familiarize ourselves in the bREPL with the above domina
functions.
First we need to require the domina.core
namespace:
cljs.user=> (require '[domina.core :refer [by-id value set-value!]])
nil
As you see, instead of aliasing the required namespace as we did in
the previous tutorial, we're now directly interning by-id
,
value
and set-value!
symbols into the cljs.user
namespace loaded
by default by the bREPL in such a way that we can use them from the
bREPL without specifying any namespace's name or alias:
cljs.user=> (doc by-id)
-------------------------
cljs.user/by-id
([id])
Returns content containing a single node by looking up the given ID
nil
cljs.user=> (by-id "email")
#object[HTMLInputElement [object HTMLInputElement]]
Pretty easy. Let's familiarize ourselves with set-value!
and value
symbols as well:
cljs.user=> (doc set-value!)
-------------------------
cljs.user/set-value!
([content value])
Sets the value of all the nodes (presumably form fields) in the given content.
nil
cljs.user=> (set-value! (by-id "email") "[email protected]")
#object[HTMLInputElement [object HTMLInputElement]]
cljs.user=> (set-value! (by-id "password") "weakpassword")
#object[HTMLInputElement [object HTMLInputElement]]
cljs.user=> (doc value)
-------------------------
cljs.user/value
([content])
Returns the value of a node (presumably a form field). Assumes content is a single node.
nil
cljs.user=> (value (by-id "email"))
"[email protected]"
cljs.user=> (value (by-id "password"))
"weakpassword"
cljs.user=> (set-value! (by-id "password") "")
#object[HTMLInputElement [object HTMLInputElement]]
cljs.user=> (set-value! (by-id "email") "")
#object[HTMLInputElement [object HTMLInputElement]]
Have you noted that as soon as the set-value!
forms are evaluated
the corresponding fields in the login form have been updated?
Now that we better understand a few domina
functions, we are going to
update the validate-form
function. Open the
login.cljs
file and update both the namespace declaration and the
validate-form
function definition as follows:
(ns modern-cljs.login
(:require [domina.core :refer [by-id value]]))
(defn validate-form []
(if (and (> (count (value (by-id "email"))) 0)
(> (count (value (by-id "password"))) 0))
true
(do (js/alert "Please, complete the form!")
false)))
As you can see, by using domina
the code is now more fluid and
idiomatic than before. Leave the rest of the file as is.
As soon as you save the changes, the IFDE triggers the CLJS compiler
and reloads the index.html
file as well.
You can safely interact with the login form which is now managed via the
domina
lib.
Require the modern-cljs.login
namespace:
cljs.user=> (require '[modern-cljs.login :as l] :reload)
nil
Then repeat the kind of experiments we did in the previous tutorial:
cljs.user=> l/validate-form
#object[modern_cljs$login$validate_form "function modern_cljs$login$validate_form(){
if(((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"email"))) > (0))) && ((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"password"))) > (0)))){
return true;
} else {
alert("Please, complete the form!");
return false;
}
}"]
cljs.user=> (l/validate-form)
false
cljs.user=> (set-value! (by-id "email") "[email protected]")
#object[HTMLInputElement [object HTMLInputElement]]
cljs.user=> (set-value! (by-id "password") "weakpassword")
#object[HTMLInputElement [object HTMLInputElement]]
cljs.user=> (l/validate-form)
true
As already shown in the previous tutorial, the evaluation of the
l/validate-form
symbol returns the JS translation generated by the
CLJS compiler from the corresponding validate-form
CLJS function
definition.
Then, when we call the validate-form
while the email
and
password
fields are empty, it returns the false
boolean value.
Finally, after having set a non-void string for both fields, the
validate-form
evaluation returns, as expected, the true
boolean
value.
Now let's try to port a second example from Larry Ullman's Modern JavaScript book to CLJS: a kind of e-commerce tool that will calculate the total of an order, including tax, minus any discount.
Here is the shopping.html
content which is in line with the
clojurescriptone approach
and Larry Ullman to keep the design
of the HTML/CSS/images separated from the code which is going to
implement its behavior. Save it in your html
directory:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Shopping Calculator</title>
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- shopping.html -->
<form action="" method="post" id="shoppingForm" novalidate>
<legend> Shopping Calculator</legend>
<fieldset>
<div>
<label for="quantity">Quantity</label>
<input type="number"
name="quantity"
id="quantity"
value="1"
min="1" required>
</div>
<div>
<label for="price">Price Per Unit</label>
<input type="text"
name="price"
id="price"
value="1.00"
required>
</div>
<div>
<label for="tax">Tax Rate (%)</label>
<input type="text"
name="tax"
id="tax"
value="0.0"
required>
</div>
<div>
<label for="discount">Discount</label>
<input type="text"
name="discount"
id="discount"
value="0.00" required>
</div>
<div>
<label for="total">Total</label>
<input type="text"
name="total"
id="total"
value="0.00">
</div>
<div>
<input type="submit"
value="Calculate"
id="submit">
</div>
</fieldset>
</form>
<script src="js/main.js"></script>
</body>
</html>
As before, we included the link to js/main.js
, the external JS file
which will be generated by the CLJS compilation. Note that this time
we have not attached any value to the action
attribute of the
form. That's because in this new example there is no server-side form
submission.
Again, as soon as you save the file, the IFDE will reload it as you
can confirm by visiting the http://localhost:3000/shopping.html
URL.
If your Developer Tool is open, the console will notify you about the following error:
Uncaught TypeError: Cannot set property 'onsubmit' of null
pertaining the login.cljs
CLJS source file. This is because
shopping.html
does not have any loginForm
id available on which to
set the value for the onsubmit
property. At the moment, we don't care
about this issue.
Before starting to define the function for calculating the total amount you're going to spend, let's play a little bit by bREPLing with the Shopping Calculator form and its field ids:
cljs.user=> (value (by-id "quantity"))
"1"
cljs.user=> (value (by-id "price"))
"1.00"
cljs.user=> (value (by-id "tax"))
"0.0"
cljs.user=> (value (by-id "discount"))
"0.00"
You can even test the calculation of the total amount from the bREPL:
cljs.user> (let [quantity (value (by-id "quantity"))
price (value (by-id "price"))
tax (value (by-id "tax"))
discount (value (by-id "discount"))]
(-> (* quantity price)
(* (+ 1 (/ tax 100)))
(- discount)
(.toFixed 2)))
"1.00"
Not so bad, eh!
If you don't know about the thread macros ->
and ->>
, I strongly suggest to
take some time to watch this awesome video tutorial.
Now it's time to freeze the above experimental tests into a source
file. Remember that we have to write back the total amount into the
shopping form and return false
to prevent the browser from
attempting to submit the data to a server-side script.
Create the shopping.cljs
file in the src/cljs/modern_cljs
directory and
enter the following code:
(ns modern-cljs.shopping
(:require [domina.core :refer [by-id value set-value!]]))
(defn calculate []
(let [quantity (value (by-id "quantity"))
price (value (by-id "price"))
tax (value (by-id "tax"))
discount (value (by-id "discount"))]
(set-value! (by-id "total") (-> (* quantity price)
(* (+ 1 (/ tax 100)))
(- discount)
(.toFixed 2)))
false))
(defn init []
(if (and js/document
(.-getElementById js/document))
(let [the-form (by-id "shoppingForm")]
(set! (.-onsubmit the-form) calculate))))
(set! (.-onload js/window) init)
Here we created the modern-cljs.shopping
namespace and required the
domina.core
namespace, referring by-id
, value
, and
set-value!
into the current namespace.
We then retrieved the values required to calculate the total amount, setting
it as the value of the total
field of the form, before finally returning
the false
boolean value.
The init
function is almost identical to the one defined for the
previous login form. At last we set the init
function itself as the
value of the onload
property of the window
object defined in the
js
special namespace.
As soon as you save the shopping.cljs
file, it gets recompiled and
the corresponding shopping.html
page is reloaded.
But we still have to make another change. We need to add the newly
created modern-cljs.shopping
namespace to the required section of
the main.cljs.edn
file living in the html/js
directory.
Open the html/js/main.cljs.edn
file and enter the above addition:
{:require [modern-cljs.core modern-cljs.login modern-cljs.shopping]
:compiler-options {:asset-path "js/main.out"}}
NOTE 4: this an incidental complexity produced by the
boot-cljs
task. Do you remember when at the beginning of this series I wrote about the idiosyncrasies of the building tools when you walk a little bit out of its defaults? This is one of them. Here you have to choose between two alternatives: either you adhere to the very widespread convention of keeping thejs
resources confined in ajs
subdirectory of the directory where your html pages live; or, to not incur the above incidental complexity, you stay with the defaults ofboot-cljs
task and abandon an almost universal convention of the web.
Go back to the bREPL and require the newly created namespace to play with the shopping calculator:
cljs.user> (require '[modern-cljs.shopping :as s] :reload)
nil
cljs.user> s/calculate
#object[modern_cljs$shopping$calculate "function modern_cljs$shopping$calculate(){
var quantity = domina.value.call(null,domina.by_id.call(null,"quantity"));
var price = domina.value.call(null,domina.by_id.call(null,"price"));
var tax = domina.value.call(null,domina.by_id.call(null,"tax"));
var discount = domina.value.call(null,domina.by_id.call(null,"discount"));
domina.set_value_BANG_.call(null,domina.by_id.call(null,"total"),(((quantity * price) * ((1) + (tax / (100)))) - discount).toFixed((2)));
return false;
}"]
cljs.user>(s/calculate)
false
As you see, the calculate
function displays the correct value in the
shopping form and returns false
as expected.
But now there is a bad surprise waiting for you at the corner that has
to do with the onsubmit
error we received before.
Let's go back to the login form by visiting the
http://localhost:3000/index.html
URL and click the Login button.
Oops, you got the Page Not Found
notification. What happened?
Revisit the index.html
page and go back to your active bREPL
session. Just to be sure, require again the modern-cljs.login
namespace and call the l/validate-form
function:
cljs.user> (require '[modern-cljs.login :as l] :reload)
nil
cljs.user> (l/validate-form)
false
cljs.user>
As you see that function is still working as expected and the previous error is not very informative. We have not yet introduced any debugging tools to be used in such a case, so we try troubleshooting with what we have in our hands.
As we saw the validate-form
function is correctly called in the
bREPL, but not by the Login
button of the form. Let's see if the
bREPL may help us in investigating the problem we have:
cljs.user> (.-onsubmit (by-id "loginForm"))
nil
Oops, the onsubmit
property of the loginForm
form element has no
value. validate-form
should have been set as its value by the init
function which, in turn, should have been set as the value of the
onload
property of the window
object. Let's now see what's the
value of the onload
property of the window
object:
cljs.user=> (.-onload js/window)
#object[modern_cljs$login$init "function modern_cljs$login$init(){
if(cljs.core.truth_((function (){var and__7101__auto__ = document;
if(cljs.core.truth_(and__7101__auto__)){
return document.getElementById;
} else {
return and__7101__auto__;
}
})())){
var login_form = document.getElementById("loginForm");
return login_form.onsubmit = modern_cljs.login.validate_form;
} else {
return null;
}
}"]
Even more mysteriously, the init
function is the correct one, i.e. the one associated to the login form.
What just happened has to do with the CLJS/Google Closure Compiler
pair. They get every CLJS file from the :source-paths
we set in the
very first tutorial and compile all of them into the single
js/main.js
file we set in the same tutorial.
To temporarily solve this problem, evaluate the init
function in
the bREPL as follows:
cljs.user> (l/init)
#object[modern_cljs$login$validate_form "function modern_cljs$login$validate_form(){
if(((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"email"))) > (0))) && ((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"password"))) > (0)))){
return true;
} else {
alert("Please, complete the form!");
return false;
}
}"]
If you now evaluate again the above expression to get the function
associated with the onsubmit
property of the loginForm
, you'll get
the right answer:
cljs.user> (.-onsubmit (by-id "loginForm"))
#object[modern_cljs$login$validate_form "function modern_cljs$login$validate_form(){
if(((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"email"))) > (0))) && ((cljs.core.count.call(null,domina.value.call(null,domina.by_id.call(null,"password"))) > (0)))){
return true;
} else {
alert("Please, complete the form!");
return false;
}
}"]
You can now use the login form as usual by clicking its Login button.
You should now stop any boot
related process and reset your git
repository:
git reset --hard
In the next tutorial we're going to investigate and solve in two different ways the problem we have just met.
Copyright © Mimmo Cosenza, 2012-2015. Released under the Eclipse Public License, the same as Clojure.