From 5c90ddf3fd9a5e730a3feaef854c3d2b79d6a4ce Mon Sep 17 00:00:00 2001 From: "James N. V. Cash" Date: Thu, 14 Nov 2013 23:37:33 -0500 Subject: [PATCH 1/5] adding dependencies for core.async --- project.clj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/project.clj b/project.clj index 3584fa35..b4339fca 100644 --- a/project.clj +++ b/project.clj @@ -16,17 +16,19 @@ :source-paths ["src/clj"] :dependencies [[org.clojure/clojure "1.5.1"] - [org.clojure/clojurescript "0.0-1847"] + [org.clojure/clojurescript "0.0-2014"] [compojure "1.1.5"] [hiccups "0.2.0"] [domina "1.0.2"] [org.clojars.magomimmo/shoreleave-remote-ring "0.3.1-SNAPSHOT"] [org.clojars.magomimmo/shoreleave-remote "0.3.1-SNAPSHOT"] [com.cemerick/valip "0.3.2"] - [enlive "1.1.4"]] + [enlive "1.1.4"] + [org.clojure/tools.reader "0.7.10"] + [org.clojure/core.async "0.1.256.0-1bf8cf-alpha"]] :plugins [[lein-ring "0.8.7"] - [lein-cljsbuild "0.3.4"]] + [lein-cljsbuild "1.0.0-alpha2"]] :hooks [leiningen.cljsbuild] From 8acf3d87bac275f592bd5227341ba1ba717329b9 Mon Sep 17 00:00:00 2001 From: "James N. V. Cash" Date: Thu, 14 Nov 2013 23:37:58 -0500 Subject: [PATCH 2/5] client-side validations part of the tutorial --- doc/tutorial-async.md | 185 ++++++++++++++++++++ src/clj/modern_cljs/shopping/validators.clj | 3 + src/cljs/modern_cljs/shopping.cljs | 34 +++- 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 doc/tutorial-async.md diff --git a/doc/tutorial-async.md b/doc/tutorial-async.md new file mode 100644 index 00000000..16d6ee8c --- /dev/null +++ b/doc/tutorial-async.md @@ -0,0 +1,185 @@ +# Tutorial XX - sync or swim + +One of the defining characteristics of the Lisp-family of languages is the +ability to take concepts from other languages and implement them at a library +level. One of the recent examples of this is the [core.async][1] library, which +adds asynchronous communication ala-[golang][2]. + +## Introduction + +In this tutorial, we're going to integrate `core.async` and use it to add +client-side validations to our shopping form. In the process, we will get to +experience a new way of thinking about and reacting to events. + +> NOTE 1: I suggest you keep track of your work by issuing the +> following commands at the terminal: +> +> ```bash +> git clone https://github.com/magomimmo/modern-cljs.git +> cd modern-cljs +> git checkout tutorial-22 +> git checkout -b tutorial-XX-step-1 +> ``` + +## Dependencies + +Since `core.async` is a fairly bleeding-edging library, we need to bump up the +versions of clojurescript and cljsbuild to make everything work. + +```clj +(defproject modern-cljs "0.1.0-SNAPSHOT" + ... + ... + :dependencies [,,, + [org.clojure/clojurescript "0.0-2014"] + ,,, + [org.clojure/core.async "0.1.256.0-1bf8cf-alpha"] + ; you may need this as well to avoid dependency problems + [org.clojure/tools.reader "0.7.10"]] + ,,, + :plugins [[lein-cljsbuild "1.0.0-alpha2"] + ,,,] +``` + +## Adding Validation + +The first thing we'll do with `core.async` is perform client-side validation of +the shopping cart form whenever a field changes. Since we'll want to be able to +validate a single form field (as opposed to the entire form), we first add a +simple method to the end of `shopping/validatiors.clj`: + +```clojure +(defn validate-field [field val] + (field (validate-shopping-form val val val val))) +``` + +A slight hack, this function validates a single field using the +`validate-shopping-form` function we wrote previously by passing in the same +value for each of the fields, then just getting the errors for the field we care +about. + +Next, we will add the new requirements to `shopping.cljs`: + +```clojure +(ns modern-cljs.shopping + (:require-macros [hiccups.core :refer [html]] + [cljs.core.async.macros :refer [go-loop]]) + (:require [domina :refer [by-id value by-class set-value! append! destroy! + add-class! remove-class! text set-text!]] + [domina.events :refer [listen! prevent-default]] + [domina.xpath :refer [xpath]] + [hiccups.runtime :as hiccupsrt] + [shoreleave.remotes.http-rpc :refer [remote-callback]] + [modern-cljs.shopping.validators :refer [validate-field]] + [cljs.core.async :refer [put! chan >! label (add-class! "error") (set-text! (first errs))) + (-> label (remove-class! "error") (set-text! title))) + (recur))))) +``` + +This function is doing quite a few things, so let's break it down. + +First, it creates a channel which will recieve errors for the field: + +```clojure + (let [errs-chan (map< (fn [evt] + (validate-field (keyword field) (.-value (:target evt)))) + (listen (by-id field) :change)) +``` + +The `map<` function takes events recieved on the channel created by `listen` +and uses the `validate-field` function to create a channel of `nil`s and error +vectors. + +```clojure + label (xpath (str "//label[@for='" field "']")) + title (text label)] +``` + +We get the `<label>` element associated with the given field using an +xpath selector and keep track of what the "default" value of the label should +be. + +```clojure +(go-loop + [] + (let [errs ( label (add-class! "error") (set-text! (first errs))) + (-> label (remove-class! "error") (set-text! title))) + (recur))) +``` + +Now, the meat of the function is the "go-routine" we create. Like in the +Go language, this creates an asynchronous process which can wait for inputs on a +channel without blocking anything else. Instead of using the basic `go` macro, +we use `go-loop`, equivalent to `(go (loop ...``, since we want to keep +processing events forever. The routine will wait for an error to come in on the +previously-created channel `errs-chan`, then either display an error message in +the label or restore the original state of the label. + +Now, we can create one of these goroutines for each field in the form in `init` +with a simple loop over the ids of the fields. + +```clojure +(defn ^:export init [] + (when (and js/document + (aget js/document "getElementById")) + (loop [fields '("quantity" "price" "tax" "discount")] + (when (not (empty? fields)) + (check-field (first fields)) + (recur (rest fields)))) + (listen! (by-id "calc") :click (fn [evt] (calculate evt))) + (listen! (by-id "calc") :mouseover add-help!) + (listen! (by-id "calc") :mouseout remove-help!))) +``` + +Now, we can do our usual incantation after making changes: + +```bash +lein clean-start! +``` + +After rebuild, go to the [shopping url][3] (making sure Javascript is enabled +now) and try putting some values in the form. You'll see that if you put an +invalid value in one of the fields, a message will appear as soon as you leave +the input and go away once corrected. + +[1]: http://clojure.github.io/core.async/ +[2]: http://golang.org/ +[3]: http://localhost:3000/shopping.html diff --git a/src/clj/modern_cljs/shopping/validators.clj b/src/clj/modern_cljs/shopping/validators.clj index 2c427c8f..d8ec7272 100644 --- a/src/clj/modern_cljs/shopping/validators.clj +++ b/src/clj/modern_cljs/shopping/validators.clj @@ -29,3 +29,6 @@ ;; cross validations (not at the moment) )) + +(defn validate-field [field val] + (field (validate-shopping-form val val val val))) diff --git a/src/cljs/modern_cljs/shopping.cljs b/src/cljs/modern_cljs/shopping.cljs index 3bee3c97..6b75bf69 100644 --- a/src/cljs/modern_cljs/shopping.cljs +++ b/src/cljs/modern_cljs/shopping.cljs @@ -1,9 +1,14 @@ (ns modern-cljs.shopping - (:require-macros [hiccups.core :refer [html]]) - (:require [domina :refer [by-id value by-class set-value! append! destroy!]] + (:require-macros [hiccups.core :refer [html]] + [cljs.core.async.macros :refer [go-loop]]) + (:require [domina :refer [by-id value by-class set-value! append! destroy! + add-class! remove-class! text set-text!]] [domina.events :refer [listen! prevent-default]] + [domina.xpath :refer [xpath]] [hiccups.runtime :as hiccupsrt] - [shoreleave.remotes.http-rpc :refer [remote-callback]])) + [shoreleave.remotes.http-rpc :refer [remote-callback]] + [modern-cljs.shopping.validators :refer [validate-field]] + [cljs.core.async :refer [put! chan >! label (add-class! "error") (set-text! (first errs))) + (-> label (remove-class! "error") (set-text! title))) + (recur))))) + (defn ^:export init [] (when (and js/document (aget js/document "getElementById")) + (loop [fields '("quantity" "price" "tax" "discount")] + (when (not (empty? fields)) + (check-field (first fields)) + (recur (rest fields)))) (listen! (by-id "calc") :click (fn [evt] (calculate evt))) (listen! (by-id "calc") :mouseover add-help!) (listen! (by-id "calc") :mouseout remove-help!))) From 74b2466103823619a39b0d5317742c75ad8c38eb Mon Sep 17 00:00:00 2001 From: "James N. V. Cash" Date: Fri, 15 Nov 2013 14:06:37 -0500 Subject: [PATCH 3/5] Doing some refactoring of the error-watching --- doc/tutorial-async.md | 80 ++++++++++++------------------ src/cljs/modern_cljs/shopping.cljs | 23 +++++---- 2 files changed, 45 insertions(+), 58 deletions(-) diff --git a/doc/tutorial-async.md b/doc/tutorial-async.md index 16d6ee8c..b073f6e4 100644 --- a/doc/tutorial-async.md +++ b/doc/tutorial-async.md @@ -93,65 +93,49 @@ The function simply creates a channel called out, uses the domina `listen!` function to register a callback which will put the recieved event into the channel, then returns the new channel. -With this function in place, we can define our function to asynchronously check -a given field. +Next, we'll define a helper function which will create a channel of the +validation errors for a given field. ```clojure -(defn check-field [field] - (let [errs-chan (map< (fn [evt] - (validate-field (keyword field) (.-value (:target evt)))) - (listen (by-id field) :change)) - label (xpath (str "//label[@for='" field "']")) - title (text label)] - (go-loop - [] - (let [errs ( label (add-class! "error") (set-text! (first errs))) - (-> label (remove-class! "error") (set-text! title))) - (recur))))) +(defn errors-for [field] + (map< (fn [evt] + (or (validate-field (keyword field) (.-value (:target evt))) + [])) + (listen (by-id field) :change))) ``` -This function is doing quite a few things, so let's break it down. - -First, it creates a channel which will recieve errors for the field: - -```clojure - (let [errs-chan (map< (fn [evt] - (validate-field (keyword field) (.-value (:target evt)))) - (listen (by-id field) :change)) -``` +The `map<` function takes events recieved on the channel created by `listen` +and uses the `validate-field` function to create a channel of error vectors +(note that we replace nils with empty lists to avoid inadvertently indicating +that the channel is exhausted). -The `map<` function takes events recieved on the channel created by `listen` -and uses the `validate-field` function to create a channel of `nil`s and error -vectors. +With these functions in place, we can define our function to asynchronously check +a given field. ```clojure - label (xpath (str "//label[@for='" field "']")) +(defn check-field [field errs-chan] + (let [label (xpath (str "//label[@for='" field "']")) title (text label)] -``` - -We get the `<label>` element associated with the given field using an -xpath selector and keep track of what the "default" value of the label should -be. - -```clojure -(go-loop + (go-loop [] (let [errs ( label (add-class! "error") (set-text! (first errs))) - (-> label (remove-class! "error") (set-text! title))) - (recur))) + (if (empty? errs) + (-> label (remove-class! "error") (set-text! title)) + (-> label (add-class! "error") (set-text! (first errs)))) + (recur))))) ``` -Now, the meat of the function is the "go-routine" we create. Like in the -Go language, this creates an asynchronous process which can wait for inputs on a -channel without blocking anything else. Instead of using the basic `go` macro, -we use `go-loop`, equivalent to `(go (loop ...``, since we want to keep -processing events forever. The routine will wait for an error to come in on the -previously-created channel `errs-chan`, then either display an error message in -the label or restore the original state of the label. +This function recieves the name of the field and a channel of errors (which +we'll create with `(errors-for field)`). We get the corresponding label for the +input field and keep track of what it's original title should be, then spawn our +go-routine with `go-loop`. + +Like in the Go language, this creates an asynchronous process which can wait for +inputs on a channel without blocking anything else. Instead of using the basic +`go` macro, we use `go-loop`, equivalent to `(go (loop ...`, since we want to +keep processing events forever. The routine will wait for an error to come in +on the `errs-chan` channel, then either display an error message in the label or +restore the original state of the label. Now, we can create one of these goroutines for each field in the form in `init` with a simple loop over the ids of the fields. @@ -162,7 +146,7 @@ with a simple loop over the ids of the fields. (aget js/document "getElementById")) (loop [fields '("quantity" "price" "tax" "discount")] (when (not (empty? fields)) - (check-field (first fields)) + (check-field (first fields) (errors-for (first fields))) (recur (rest fields)))) (listen! (by-id "calc") :click (fn [evt] (calculate evt))) (listen! (by-id "calc") :mouseover add-help!) diff --git a/src/cljs/modern_cljs/shopping.cljs b/src/cljs/modern_cljs/shopping.cljs index 6b75bf69..a0ef63e8 100644 --- a/src/cljs/modern_cljs/shopping.cljs +++ b/src/cljs/modern_cljs/shopping.cljs @@ -8,7 +8,7 @@ [hiccups.runtime :as hiccupsrt] [shoreleave.remotes.http-rpc :refer [remote-callback]] [modern-cljs.shopping.validators :refer [validate-field]] - [cljs.core.async :refer [put! chan >! ! label (add-class! "error") (set-text! (first errs))) - (-> label (remove-class! "error") (set-text! title))) + (if (empty? errs) + (-> label (remove-class! "error") (set-text! title)) + (-> label (add-class! "error") (set-text! (first errs)))) (recur))))) (defn ^:export init [] @@ -52,7 +55,7 @@ (aget js/document "getElementById")) (loop [fields '("quantity" "price" "tax" "discount")] (when (not (empty? fields)) - (check-field (first fields)) + (check-field (first fields) (errors-for (first fields))) (recur (rest fields)))) (listen! (by-id "calc") :click (fn [evt] (calculate evt))) (listen! (by-id "calc") :mouseover add-help!) From 712b5b2251ff8205b11046903f51205477d293d3 Mon Sep 17 00:00:00 2001 From: "James N. V. Cash" Date: Fri, 15 Nov 2013 14:24:02 -0500 Subject: [PATCH 4/5] Adding writeup and code for automatic total updating --- doc/tutorial-async.md | 98 +++++++++++++++++++++++++++++- src/cljs/modern_cljs/shopping.cljs | 31 +++++++--- 2 files changed, 116 insertions(+), 13 deletions(-) diff --git a/doc/tutorial-async.md b/doc/tutorial-async.md index b073f6e4..a395ecaf 100644 --- a/doc/tutorial-async.md +++ b/doc/tutorial-async.md @@ -144,10 +144,11 @@ with a simple loop over the ids of the fields. (defn ^:export init [] (when (and js/document (aget js/document "getElementById")) - (loop [fields '("quantity" "price" "tax" "discount")] - (when (not (empty? fields)) + (let [fields '("quantity" "price" "tax" "discount")] + (loop [fields fields] + (when (not (empty? fields)) (check-field (first fields) (errors-for (first fields))) - (recur (rest fields)))) + (recur (rest fields))))) (listen! (by-id "calc") :click (fn [evt] (calculate evt))) (listen! (by-id "calc") :mouseover add-help!) (listen! (by-id "calc") :mouseout remove-help!))) @@ -164,6 +165,97 @@ now) and try putting some values in the form. You'll see that if you put an invalid value in one of the fields, a message will appear as soon as you leave the input and go away once corrected. +## Automatic updates + +Now that we have our error messages appearing right away, it's starting to feel +clunky to have to click the "Calculate" button every time we make a change. +Let's leverage the techniques we've just learned to automatically recalculate +the total when the values change. The one complication, however, is that we +don't want to try to calculate a new total if any of the fields are invalid. + +First, we'll need to add the `merge` function from `core.async` to our list of +requires: + +```clojure +(ns modern-cljs.shopping + ... + (:require ,,, + [cljs.core.async :refer [put! chan >! ! label (add-class! "error") (set-text! (first errs)))) (recur))))) +(defn recalc-total [errs-chan] + (go-loop + [err-fields #{}] + (let [[field errs] ( Date: Fri, 15 Nov 2013 14:25:33 -0500 Subject: [PATCH 5/5] Adding footer stub --- doc/tutorial-async.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/tutorial-async.md b/doc/tutorial-async.md index a395ecaf..44d0e021 100644 --- a/doc/tutorial-async.md +++ b/doc/tutorial-async.md @@ -256,6 +256,14 @@ using `map<` and `cljs.core.async/merge` in `init` as follows: Now, run `lein compile` and refresh and you will see the shopping form automatically updating the total whenever a valid change is made. +# Next Step + +TO BE DONE + +# License + +By James Cash, for Mimmo Cosenza to do whatever he wishes with. + [1]: http://clojure.github.io/core.async/ [2]: http://golang.org/ [3]: http://localhost:3000/shopping.html