...
<div>
<label for="submit"></label>
<input type="submit" value="Login →" id="submit">
</div>
...
listen! 函數
;;; namespace declaration
(ns modern-cljs.login
(:require [domina.core :refer [by-id value]]
[domina.events :refer [listen!]]))
;;; init
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(listen! (by-id "submit") :click validate-form)))
domina.events 中的 prevent-default 幫忙處理這件事domina.events :
(ns modern-cljs.login
(:require [domina.core :refer [by-id value]]
[domina.events :refer [listen! prevent-default]]))
validate-form 擋下提交帳號密碼到伺服器端:
(defn validate-form [e]
(if (or (empty? (value (by-id "email")))
(empty? (value (by-id "password"))))
(do
(prevent-default e)
(js/alert "Please, complete the form!"))
true))
validate-form 不會像原本的一樣回傳 false
(defn validate-form [e]
(if (or (empty? (value (by-id "email")))
(empty? (value (by-id "password"))))
(do
(prevent-default e)
(js/alert "Please, complete the form!")) ;; return false?
true))
prevent-default 擋下表單提交false 來做進一步處理validate-form 需要輸入參數,也就是事件 e 才能擋下提交:
(defn validate-form [e]
...
(prevent-default e)
(js/alert "Please, complete the form!"))
)
init 函數需要做一個匿名函數放入事件參數
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(listen! (by-id "submit") :click (fn [e] (validate-form e)))))
:dynamic 的正則驗證的變數:dynamic 時,我們就不需要傳遞正則表達式到底層的驗證函數
;;; 4 to 8, at least one numeric digit.
(def ^:dynamic *password-re*
#"^(?=.*\d).{4,8}$")
(def ^:dynamic *email-re*
#"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")
(defn ^:export init []
(if (and js/document
(aget js/document "getElementById"))
(let [email (by-id "email")
password (by-id "password")]
...
(listen! email :blur (fn [evt] (validate-email email)))
(listen! password :blur (fn [evt] (validate-password password))))))
validate-email 和 validate-password 就是我接著要實現的正則表達式的驗證函數
(defn validate-email [email]
(destroy! (by-class "email"))
(if (not (re-matches *email-re* (value email)))
(do (prepend! (by-id "loginForm") (html [:div.help.email "Wrong email"]))
false) true))
...
(defn validate-password [password]
(destroy! (by-class "password"))
(if (not (re-matches *password-re* (value password)))
(do (append! (by-id "loginForm") (html [:div.help.password "Wrong password"]))
false) true))
index.html
<form action="login.php" method="post" id="loginForm">
...
<input type="email" name="email" id="email"
placeholder="email"
title="Type a well-formed email!"
pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
required>
...
pattern 在做的事情就是我們前面有做過的驗證domina 的做法完成這件事(後略)core.clj 應該對 routes 有定義,來處理表單提交的 POST
(ns modern-cljs.core
(:require [compojure.core :refer [defroutes GET POST]] ; <- add POST
[compojure.route :refer [not-found files resources]]))
...
(defroutes handler
(GET "/" [] "Hello from Compojure!") ;; for testing only
(files "/" {:root "target"}) ;; to serve static resources
(POST "/login" [email password] (authenticate-user email password)) ; <- add POST route
(resources "/" {:root "target"}) ;; to serve anything else
(not-found "Page Not Found")) ;; page not found
login.clj 做和 login.cljs 一樣的事authenticate-user 函數:
(defn authenticate-user [email password]
(if (or (empty? email) (empty? password))
(str "Please complete the form")
(if (and (validate-email email)
(validate-password password))
(str email " and " password
" passed the formal validation, but you still have to be authenticated"))))
core.clj 在伺服器端使用
(ns modern-cljs.core
(:require [compojure.core :refer [defroutes GET POST]]
[compojure.route :refer [not-found files resources]]
[modern-cljs.login :refer [authenticate-user]]))
...
login.clj 的程式碼和 login.cljs 重複Tutorial 12
由於 Clojure 的蓬勃發展,現在(2017)已經有許多 cljs 的驗證函式庫
Valip 函式庫有什麼功能validate :
(validate {:key-1 hvalue-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])
(validate {:email "you@yourdomain.com" :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"])
:email 與 :passwordpresent? 驗證是否存在,也就是是否為空email-address? 則是透過 Valip 函式庫定義驗證是否為 email
(validate {:email "you@yourdomain.com" :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"])
nilvalip 函式庫,你會發現要自定義自己的 predicates 與函數並不困難present? 在 valip 的 namespace 中很清楚:(defn present? [x] (not (string/blank? x)))
nil 時 ,可能造成 NullPointerException(defn matches [re] (fn [s] (boolean (re-matches re s))))
s ,應該寫成 (str s) :(defn matches [re] (fn [s] (boolean (re-matches re (str s)))))
defpredicate macro,這是 valip 的範例之一:
(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"))))
(ns valip.predicates
(: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]))
valip.predicates 的 namespaceboot 處理,比起其他方法更容易處理移植問題build.boot 中添加 valip 依賴:
(set-env!
...
:dependencies '[...
[org.clojars.magomimmo/valip "0.4.0-SNAPSHOT"]
])
valip.core 與 valip.predicates :(use 'valip.core 'valip.predicates)
boot.user> (validate {:email "you@yourdomain.com" :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
nil 也就是全部驗證都 pass
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"]}
valip.core 與 valip.predicates :
(ns modern-cljs.login.validators
(:require [valip.core :refer [validate]]
[valip.predicates :refer [present? matches email-address?]]))
valip.predicates 的原因是 valip 提供predicates 的正則表達式validators 的 namespaceauthenticate-user 就可以:
(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")))
(誤)
.cljc 的檔案,會特別進行功能識別(feature condition)#? 和 #?@#? 後面,可透過 clj, cljs 和 clr 做編譯期(compile-time)的註明,其中:
:clj 會被識別為 JVM:cljs 會被識別為 JSVM:clr 會被識別回 Microsoft 的 CLR(也就是 .NET )valip.predicates 為 non-portable 的話 :
#? (:clj (defn email-domain-errors [email]
(validate
{:email email}
[:email pred/valid-email-domain? ;; valip.predicates as pred
"The domain of the email doesn't exist."])))
cljc 中cljc 目錄下並改名為 cljc 後綴build.boot 檔案,並重新啟動 boot :
(set-env!
:source-paths #{"src/clj" "src/cljs" "src/cljc"}
...
)
boot.user=> (start-repl) ... 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 "me@me.com" "weak1")
nil
(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]]))
(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))
nil ,回傳的錯誤也只收 :emailvalidate-password , validate-form 以及 initinit 時,由於他會直接編譯到 index.html 中引用的 js ,所以需要手動重新整理頁面
(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 email-domain-errors [email]
(v/email-domain-errors email))
:refer 而是 :as 因為在伺服器端以及 remote 保持同樣的名字
(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]]
...
[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)))