Interacting With JavaScript and Web Pages

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:

  1. Direct use of JavaScript
  2. The Google Closure library
  3. The Dommy library
  4. The Domina library
  5. The Enfocus library
Note
All of these methods are fairly “old school.” As of this writing, all the Cool Kids are using libraries such as Facebook’s React to handle the user interface. I still think it is useful to have knowledge of the older methods, as they might sometimes be the right tool to solve a problem. However, if you want, see Programming with React.

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" />&#176;<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.

Note
If your project name has a hyphen in it, such as 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.

Étude 2-1: Direct use of JavaScript

This is the most direct method to interact with a page, and is the least ClojureScript-like in its approach.

Invoking Methods

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

Accessing Properties

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)

Creating JavaScript Objects

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)

Listening for Events

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

Étude 2-2: Using Google Closure

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.

Putting Google Closure into Your Project

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.

Using Google Closure to Access the DOM

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

Using Google Closure to Handle Events

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

Étude 2-3: Using dommy

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.

Putting dommy into Your Project

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"]]

Using dommy to Access the DOM

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

Using dommy to Handle Events

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

Étude 2-4: Using Domina

The Domina library is very similar in approach to dommy. In this étude, you will use Domina to interact with the web page.

Putting Domina into Your Project

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"]]

Using Domina to Access the DOM

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

Using Domina to Handle Events

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

Étude 2-5: Using Enfocus

The Enfocus library is very different from dommy and Domina.

Putting Enfocus into Your Project

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"]]

Using Enfocus to Access the DOM

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

Using Enfocus to Handle Events

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