... <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
與 :password
present?
驗證是否存在,也就是是否為空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"])
nil
valip
函式庫,你會發現要自定義自己的 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
也就是全部驗證都 passboot.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
,回傳的錯誤也只收 :email
validate-password
, validate-form
以及 init
init
時,由於他會直接編譯到 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)))