Since ClojureScript compiles to JavaScript, you need to have a way to interact with native JavaScript and with web pages. In this chapter, you will find out five different ways to do this:
You’ll be doing the same task with each of these: calculating the number of hours of daylight based on a latitude and Julian date, as in Étude 1-5: More Practice with def and let. Here is the relevant HTML:
<!DOCTYPE html>
<html>
<head>
<title>
Daylight Minutes</title>
<meta
http-equiv=
"Content-Type"
content=
"text/html; charset=utf-8"
/>
</head>
<body>
<h1>
Daylight Minutes</h1>
<p>
Latitude:<input
type=
"text"
size=
"8"
id=
"latitude"
/>
°
<br
/>
Day of year:<input
type=
"text"
size=
"4"
id=
"julian"
/><br
/>
<input
type=
"button"
value=
"Calculate"
id=
"calculate"
/>
</p>
<p>
Minutes of daylight:<span
id=
"result"
></span>
</p>
<script
src=
"out/<em>project_name</em>.js"
type=
"text/javascript"
></script>
</body>
</html>
I suggest you create a new project for each of these études and copy the preceding HTML into the project’s index.html file. Remember to make the
src
attribute of the script
element match your project name.
my-project
, Clojure and ClojureScript will convert the hyphens to underscores when creating directories, so you will end up with a src/my_project
directory.
This is the most direct method to interact with a page, and is the least ClojureScript-like in its approach.
In order to invoke JavaScript methods directly, you use expressions of the general form:
(
.<em>methodname</em>
<em>JavaScript
object</em>
<em>arguments</em>
)
Here are some examples you can try in the REPL.
;; call the sqrt function from JavaScript's Math object with an argument 3
(
.sqrt
js/Math
3
)
;; equivalent of window.parseFloat("3.5")
(
.parseFloat
js/window
"3.5"
)
;; equivalent of "shouting".toUpperCase()
(
.toUpperCase
"shouting"
)
;; equivalent of "ClojureScript".substr(2,3)
(
.substr
"ClojureScript"
2
3
)
;; equivalent of document.getElementById("latitude")
(
.getElementById
js/document
"latitude"
)
You can also use a different form for methods that belong to the special js
namespace. (It is not a real ClojureScript namespace, as it references the underlying JavaScript structure rather than ClojureScript code).
;; call the sqrt function from JavaScript's Math object with an argument 3
(
js/Math.sqrt
3
)
;; equivalent of window.parseFloat("3.5")
(
js/Window.parseFloat
"3.5"
)
;; equivalent of document.getElementById("latitude")
(
js/document.getElementById
"latitude"
)
To access an object’s properties, use .-
; before you try these in the browser REPL, type something into the latitude
field in the form.
;; equivalent of Math.PI
(
.-PI
js/Math
)
;; equivalent of "ClojureScript".length
(
.-length
"ClojureScript"
)
;; equivalent of document.getElementById("latitude").value
(
.-value
(
.getElementById
js/document
"latitude"
))
;; setting properties: equivalent of document.getElementById("latitude").value = 23.5;
(
set!
(
.-value
(
.getElementById
js/document
"latitude"
))
23.5
)
This étude doesn’t need you to create any JavaScript objects, but if you are interacting with an existing library you may need to do so. To create an object, give the class name followed by a period:
;; equivalent of d = new Date
(
def
d
(
js/Date.
))
;; now you can use it
(
.getHours
d
)
;; if you need a true JavaScript Array object
(
def
arr
(
js/Array.
10
20
30
))
(
get
arr
2
)
In JavaScript, if you want an HTML element to respond to an event, you add an event listener to that element, tell it what type of event you want to listen for, and give it the name of a function that handles the event. That event handling function must have one parameter to hold the event object. In ClojureScript, you need to define functions before you use them, so you have to write the event handler first and then invoke addEventListener
. Here is an example of what I did in the REPL (my project name was daylight-js
.
cljs.user=> (in-ns 'daylight-js.core) nil daylight-js.core=> (defn testing [evt] (.alert js/window "You clicked me!!!")) #<function daylight-js$core$testing(evt){ return window.alert("You clicked me!!!"); }> daylight-js.core=> (let [btn (.getElementById js/document "calculate")] (.addEventListener btn "click" testing)) nil
The first line switches to the correct namespace for the project. The second line defines the event handler, which calls JavaScript’s alert()
function to display a message. The third line tells the “Calculate” button to listen for click
events and call the testing
function when they occur.
Given this information, complete the code for the project such that, when you click the “Calculate” button, the program will read the values from the latitude and
Julian day field, calculate the number of daylight hours, and place the result in the <span id="result">
. (Hint: use the innerHTML
property.) You may also want to write a function that takes a form field name as its argument and returns the floating-point value from that field.
See a suggested solution: Solution 2-1
Using JavaScript directly is all well and good; one advantage is that (if you’re a JavaScript programmer), you already know this stuff. The bad news is that you have all the problems of getting JavaScript to work on multiple browsers and platforms. Enter Google Closure, a library of JavaScript utilities that has all of those nasty compatibility parts all figured out for you. In this étude, you’ll use Closure for the interaction.
To use Google Closure, you need to change the first lines of your core.cljs file to require the code that maniuplates the DOM and handles events. In this example, the project has been named daylight-gc
.
(
ns
daylight-gc.core
(
:require
[
clojure.browser.repl
:as
repl
]
[
goog.dom
:as
dom
]
[
goog.events
:as
events
]))
In the REPL, type (require 'goog.dom :as dom)
to access the code.
When accessing DOM elements, the main difference between Closure and pure JavaScript is that you use dom/getElement
instead of .getElementById js/document
. Thus, after starting the browser REPL and typing 55 into the latitude input area:
cljs.user=> (require 'daylight-gc.core) nil cljs.user=> (in-ns 'daylight-gc.core) nil daylight-gc.core=> (require '[goog.dom :as dom]) nil daylight-gc.core=> (dom/getElement "latitude") #<[object HTMLInputElement]> daylight-gc.core=> (.-value (dom/getElement "latitude")) "55" daylight-gc.core=> (set! (.-value (dom/getElement "latitude")) -20) -20 daylight-gc.core=> ;; Closure has its own way to set an element's text daylight-gc.core=> (dom/setTextContent (dom/getElement "result") "Here is some text") nil
Again, the code is quite similar to what you would do with plain JavaScript; you use events/listener
instead of .addListener
. The following adds a listener to the “Calculate” button.
daylight-gc.core=> (defn testing [evt] (.alert js/window "Clickety-click")) #'daylight-gc.core/testing daylight-gc.core=> (events/listen (dom/getElement "calculate") "click" testing) #<[object Object]>
After you test it, you may want to remove the listener so that it doesn’t interfere with the code you put in your source core.cljs file.
daylight-gc.core=> (events/unlisten (dom/getElement "calculate") "click" testing) true
Given this information, complete the code for the project. Note: If you created a new project and just copy/pasted the index.html file, make sure you change the <script>
element to refer to the right file.
See a suggested solution: Solution 2-2
While Google Closure gives you a lot of great code, it’s still JavaScript, and it “feels” like JavaScript. What you would like is a library that gives you the capabilities but in a more functional way. One of those libraries is dommy. In this étude, you will use dommy to interact with the web page.
To use dommy, you need to change the first lines of your core.cljs file to require the code that maniuplates the DOM and handles events. In this example, the project has been named daylight-dommy
.
(
ns
daylight-dommy.core
(
:require
[
clojure.browser.repl
:as
repl
]
[
dommy.core
:as
dommy
:refer-macros
[
sel
sel1
]]))
The :refer-macros
is new, and beyond the scope of this book. The oversimplified explanation is that ClojureScript macros are like functions with extra super powers. I will explain the sel
and sel1
later.
You also need to change the project.clj file to specify dommy as one of your project’s dependencies. The additional code is highlighted:
:dependencies [[org.clojure/clojure "1.7.0-beta2"] [org.clojure/clojurescript "0.0-3211"] [prismatic/dommy "1.1.0"]]
Dommy has two functions for accessing elements: sel1
and sel
. sel1
will return a single HTML node; sel
will return a JavaScript array of all matching elements. The index.html file has three <input/>
elements. Compare the results:
cljs.user=> ;; set up name spaces cljs.user=> (require 'daylight-dommy.core) nil cljs.user=> (in-ns 'daylight-dommy.core) nil daylight-dommy.core=> (require '[dommy.core :as dommy :refer-macros [sel sel1]]) nil daylight-dommy.core=> ;; access the first <input> element daylight-dommy.core=> (sel "input") #<[object HTMLInputElement]> daylight-dommy.core=> ;; access all the <input> elements daylight-dommy.core=> (sel1 "input") #js [#<[object HTMLInputElement]> #<[object HTMLInputElement]> #<[object HTMLInputElement]>] daylight-dommy.core=> ;; since IDs are unique, you use sel1 for them. daylight-dommy.core=> (sel1 "#latitude") #<[object HTMLInputElement]>
To access values of form fields, use dommy’s value
and set-value!
functions. (I typed 55 into the latitude field before doing these commands.) Similarly, text
and set-text!
let you read and write text content of elements. html
and set-html!
let you read and write HTML content of an element. Notice that you can use either a string or a keyword as an argument to sel
.
daylight-dommy.core=> ;; retrieve and set form field daylight-dommy.core=> (dommy/value (sel1 "#latitude")) "55" daylight-dommy.core=> (dommy/set-value! (sel1 "#latitude") 10.24) #<[object HTMLInputElement]> daylight-dommy.core=> ;; set and retrieve text content daylight-dommy.core=> (dommy/set-text! (sel1 :#result) "some text") #<[object HTMLSpanElement]> daylight-dommy.core=> (dommy/text (sel1 :#result)) "some text" daylight-dommy.core-> (dommy/set-html! (sel1 :#result) "<i>Yes!</i>")
Here is the code to add and remove an event listener. You may use either keywords or strings for event names. If you use a keyword for the event name, such as :click
when you listen for events, you must use a keyword when you remove the listener.
daylight-dommy.core=> (defn testing [event] (.alert js/window "Clicked.")) #'daylight-dommy.core/testing daylight-dommy.core=> (dommy/listen! (sel1 :#calculate) :click testing) #<[object HTMLInputElement]> daylight-dommy.core=> ;; the web page should now respond to clicks. Try it. daylight-dommy.core=> ;; now remove the listener. daylight-dommy.core=> (dommy/unlisten! (sel1 "#calculate") :click testing) #<[object HTMLInputElement]> daylight-dommy.core=>
Given this information, complete the code for the project. Note: If you created a new project and just copy/pasted the index.html file, make sure you change the <script>
element to refer to the right file.
See a suggested solution: Solution 2-3
The Domina library is very similar in approach to dommy. In this étude, you will use Domina to interact with the web page.
To use Domina, you need to change the first lines of your core.cljs file to require the code that maniuplates the DOM and handles events. In this example, the project has been named daylight-domina
.
(
ns
daylight-domina.core
(
:require
[
clojure.browser.repl
:as
repl
]
[
domina
]
[
domina.events
:as
events
]))
You also need to change the project.clj file to specify domina as one of your project’s dependencies. The additional code is highlighted:
:dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/clojurescript "1.7.48"] [domina "1.0.3"]]
In Domina, you can access an item by its ID, by a CSS class, or by an XPath expression. This étude only uses the first of these methods with the by-id
function.
cljs.user=> ;; set up name spaces cljs.user=> (require 'daylight-domina.core) nil cljs.user=> (in-ns 'daylight-domina.core) nil daylight-domina.core=> (require 'domina) nil daylight-domina.core=> (require '[domina.events :as events]) nil daylight-domina.core=> (domina/by-id "latitude") #<[object HTMLInputElement]>
To access values of form fields, use Domina’s value
and set-value!
functions. (I typed 55 into the latitude field before doing these commands.) Similarly, text
and set-text!
let you read and write text content of elements. html
and set-html!
let you read and write HTML content of an element. Notice that you can use either a string or a keyword as an argument to sel
.
daylight-domina.core=> ;; retrieve and set form field daylight-domina.core=> (domina/value (domina/by-id "latitude")) "55" daylight-domina.core=> (domina/set-value! (domina/by-id "latitude") 10.24) #<[object HTMLInputElement]> daylight-domina.core=> ;; set and retrieve text content daylight-domina.core=> (domina/set-text! (domina/by-id :result) "Testing 1 2 3") #<[object HTMLSpanElement]> daylight-domina.core=> (def resultspan (domina/by-id :result)) ;; to save typing #<[object HTMLSpanElement]> daylight-domina.core=> (domina/text resultspan) "Testing 1 2 3" daylight-domina.core-> (domina/set-html! resultspan "<i>Yes!</i>")# <[object HTMLSpanElement]> daylight-domina.core=> ;; look at web page to see result
Here is the code to add and remove an event listener. You may use either keywords or strings for event names. You may use either a string or keyword when you remove the listener. The unlisten!
function removes all listeners associated with the event type.
daylight-domina.core=> (defn testing [event] (.alert js/window "You clicked me.")) #'daylight-domina.core/testing daylight-domina.core=> (events/listen! (domina/by-id "calculate") :click testing) #<[object HTMLInputElement]> daylight-domina.core=> ;; the web page should now respond to clicks. Try it. daylight-domina.core=> ;; now remove the listener. daylight-domina.core=> (events/unlisten! (domina/by-id "calculate") "click") #<[object HTMLInputElement]> daylight-domina.core=>
Given this information, complete the code for the project. Note: If you created a new project and just copy/pasted the index.html file, make sure you change the <script>
element to refer to the right file.
See a suggested solution: Solution 2-4
The Enfocus library is very different from dommy and Domina.
To use Enfocus, you need to change the first lines of your core.cljs file to require the code that maniuplates the DOM and handles events. In this example, the project has been named daylight-enfocus
.
(
ns
daylight-dommy.core
(
:require
[
clojure.browser.repl
:as
repl
]
[
enfocus.core
:as
ef
]
[
enfocus.events
:as
ev
]))
You also need to change the project.clj file to specify Enfocus as one of your project’s dependencies. The additional code is highlighted:
:dependencies [[org.clojure/clojure "1.7.0-beta2"] [org.clojure/clojurescript "0.0-3211"] [enfocus "2.1.0"]]
The idea behind Enfocus is that you select a node and then do transformations on it. This is a very powerful concept, but this étude will use only its simplest forms. First, set up namespaces:
cljs.user=> (require 'daylight-enfocus.core) nil cljs.user=> (in-ns 'daylight-enfocus.core) nil daylight-enfocus.core=> (require '[enfocus.core :as ef]) nil daylight-enfocus.core=> (require '[enfocus.events :as ev]) nil
Enfocus lets you select an element by its ID either as a CSS selector, an Enlive selector, or an XPath Selector. In this case, let’s just stick with the old familar CSS form. To access values of form fields, use Enfocus’s from
function to select the field, then use the get-prop
transformation to extract the value. (I typed 55 into the latitude field before doing these commands.) Similarly, at
selects an element you want to alter, and the content
and html-content
transformation lets you set an element’s content.
daylight-enfocus.core=> (ef/from "#latitude" (ef/get-prop :value)) "55" daylight-enfocus.core=> (ef/at "#latitude" (ef/set-prop :value 10.24)) nil daylight-enfocus.core=> (ef/at "#result" (ef/content "New text")) nil daylight-enfocus.core=> (ef/at "#result" (ef/html-content "<i>Improved text</i>")) nil daylight-enfocus.core=> ;; look at web page to see result
Note: when you use the content
transformation, the argument must be a string or a node. You can’t use a number―you must convert it to a string:
daylight-enfocus.core=> (ef/at "#result" (ef/content (.toString 3.14159))) nil
Here is the code to add and remove an event listener.
daylight-enfocus.core=> (defn testing [evt] (.alert js/window "Click-o-rama")) #'daylight-enfocus.core/testing daylight-enfocus.core=> (ef/at "#calculate" (ev/listen :click testing)) nil daylight-enfocus.core=> ;; the web page should now respond to clicks. Try it. daylight-enfocus.core=> ;; now remove the listener. daylight-enfocus.core=> (ef/at "#calculate" (ev/remove-listeners :click)) nil
Given this information, complete the code for the project. Note: If you created a new project and just copy/pasted the index.html file, make sure you change the <script>
element to refer to the right file.
See a suggested solution: Solution 2-5