Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
Checking mergeability… Don't worry, you can still create the pull request.
  • 15 commits
  • 4 files changed
  • 0 commit comments
  • 2 contributors
View
96 readme.md
@@ -118,6 +118,102 @@ Get the values of all `<input>` elements on the page:
For examples of every currently implemented function, see the `test.cljs` file in the code repository, which exercises each function in unit tests against a DOM page. The `test.html` file loads and runs `test.cljs` in the context of the DOM.
+## Event Handling
+
+Domina contains a robust event handling API that wraps the Google Closure event handling code, while exposing it in a idiomatic functional way.
+
+### Event Propagation
+
+In Domina, every event has a *target*. This is the DOM node that is logically "causing" the event. All events triggered by the browser (such as clicks or key presses) are associated with a node. User defined events must also specify a target node.
+
+ Event listeners are also attached to nodes, and may trigger on either the *capture* or *bubble* phases of event propegation. The capture phase starts at the root node of the document, and successively fires any listeners on ancestors of the target node from the top down, down to the event target itself. In the bubble phase, the process is reversed, first firing listeners on the target node, then on each of its ancestors in succession back to the document root.
+
+### Registering Event Listeners
+
+Use the `listen!` function to register an event handler in the bubble phase, and `capture!` to register a handle for the capture phase. Both take similar argument: a Domina DomContent, the event type, and a listener function. They return a sequence of event handler keys (see section below on de-registering event handlers)
+
+```clojure
+(listen! (sel "button") :click (fn [evt] (log "button clicked!")))
+```
+
+This above snippet adds an event handler to every `<button>` element on the page, which logs a message when the button is clicked.
+
+Note that the content argument is optional: in this case, the listener is added to the root node of the document, and will catch all click events on the entire page.
+
+```clojure
+(listen! :click (fn [evt] (log "button clicked!")))
+```
+
+### Event Objects
+
+When an event is triggered, it invokes the provided listener function, passing it an event object. The event object will implement ClojureScript's `ILookup` protocol, as well as the `domina.events.Event` protocol.
+
+Implementing the `ILookup` protocol makes it easy to pull values from browser events using ClojureScript's built in lookup functions such as `get` and `contains?`, as well as using keywords in function position. Note that although native events only have string keys, Domina will attempt to translate keywords to strings for lookup purposes.
+
+```clojure
+(defn sample-click-handler [evt]
+ (let [x (:clientX evt)
+ y (:clientY evt)]
+ (log (str "Click occurred at window coordinates " x "," y))))
+```
+
+The `domina.events.Event` protocol supports the following methods:
+
+<table>
+ <tr><th>Method</th><th>Effect</th></tr>
+ <tr>
+ <td>`prevent-default`</td><td>Prevents the default action for an event from firing. For example, if you invoke `prevent-default` on a click event on a link, it will prevent the browser from navigating the browser as it normally would with a clicked link</td>
+ </tr>
+ <tr>
+ <td>`stop-propagation`</td><td>Prevents all future event listeners (in both the bubble and capture phases) from recieving the event.</td>
+ </tr>
+ <tr>
+ <td>`target`</td><td>Returns the target node of the event.</td>
+ </tr>
+ <tr>
+ <td>`current-target`</td><td>Returns the current target of the event (the node to which the current listener was attached).</td>
+ </tr>
+ <tr>
+ <td>`event-type`</td><td>Returns the type of the event</td>
+ </tr>
+ <tr>
+ <td>`raw-event`</td><td>Returns the underlying `goog.events.Event` object, rather than it's Domina wrapper.</td>
+ </tr>
+</table>
+
+### De-registering Event Handlers
+
+There are several ways to de-register an event handler.
+
+If you have they key returned by the registration function, you can de-register the handler by calling the `unlisten-by-key!` function, passing it the key as a single argument.
+
+If you do not have the key in hand, you can remove all listeners from a node (or set of nodes) using the `unlisten!` function. It takes a DomContent, and an optional event type. If the event type is specified, it will only de-register handlers for that type, otherwise it will de-register everything from the specified node.
+
+```clojure
+(unlisten! (sel "button") :click) ; removes all click event handlers from all button elements
+(unlisten! (sel "button")) ; removes all event handlers of any type from all button elements
+```
+
+There are also `listen-once!` and `capture-once!` variants of `listen!` and `capture!` which de-register themselves after the first time they are triggered.
+
+### Custom Events
+
+In addition to native events dispatched by the browser, Domina allows you to create and dispatch arbitary events using the `dispatch!` function.
+
+The `dispatch!` function takes an event target as a DomContent (assumed to be a single node), an event type, and an event map. Keys and values in the event map are merged in to the event object, and can be used to pass arbitrary data to event handlers.
+
+```clojure
+(dispatch! (by-id "evt-target") :my-event {:some-key "some value"})
+```
+
+The event will be propegated through the capture and bubble phases just like a browser event, and can be caught in the normal way:
+
+```clojure
+(listen! (by-id "evt-target") :my-event (fn [evt] (log (:some-key evt))))
+```
+
+Note that if you omit the event target when calling `dispatch!` (or when registering a listener), it will default to the root node of the document. This is often desirable when using custom application-wide events that have no logical mapping to any particular location in the DOM tree.
+
## Important note on browser XPath compatibility (IE and Android).
Internet Explorer does not support DOM Level 3 XPath selectors. In order to utilize the `domina.xpath` namespace, you will need to include a pure-javascript XPath DOM Level 3 implementation.
View
34 src/cljs/domina.cljs
@@ -106,6 +106,10 @@
(when (and *debug* (not (= (.-console js/window) js/undefined)))
(.log js/console mesg)))
+(defn log [mesg]
+ (when (.-console js/window)
+ (.log js/console mesg)))
+
(defn by-id
"Returns content containing a single node by looking up the given ID"
[id]
@@ -327,16 +331,18 @@
[content]
(. (single-node content) -innerHTML))
-(defn set-html!
- "Sets the innerHTML value for all the nodes in the given content."
+(defn- replace-children!
+ [content inner-content]
+ (-> content
+ destroy-children!
+ (append! inner-content)))
+
+(defn- set-inner-html!
[content html-string]
(let [allows-inner-html? (not (re-find re-no-inner-html html-string))
leading-whitespace? (re-find re-leading-whitespace html-string)
tag-name (. (str (second (re-find re-tag-name html-string))) (toLowerCase))
- special-tag? (contains? wrap-map tag-name)
- fallback #(-> content
- destroy-children!
- (append! %))]
+ special-tag? (contains? wrap-map tag-name)]
(if (and allows-inner-html?
(or
support/leading-whitespace?
@@ -348,9 +354,16 @@
(events/removeAll node)
(set! (. node -innerHTML) value))
(catch Exception e
- (fallback value))))
- (fallback html-string)))
- content)
+ (replace-children! content value))))
+ (replace-children! content html-string))
+ content))
+
+(defn set-html!
+ "Sets the innerHTML value for all the nodes in the given content."
+ [content inner-content]
+ (if (string? inner-content)
+ (set-inner-html! content inner-content)
+ (replace-children! content inner-content)))
(defn get-data
"Returns data associated with a node for a given key. Assumes
@@ -424,7 +437,8 @@
(defn- array-like?
[obj]
- (and (.-length obj)
+ (and obj
+ (.-length obj)
(or (.-indexOf obj) (.-item obj))))
(defn normalize-seq
View
250 src/cljs/domina/events.cljs
@@ -1,117 +1,151 @@
(ns domina.events
(:require [domina :as domina]
+ [goog.object :as gobj]
[goog.events :as events]))
+;; Restatement of the the GClosure Event API.
+(defprotocol Event
+ (prevent-default [evt] "Prevents the default action, for example a link redirecting to a URL")
+ (stop-propagation [evt] "Stops event propagation")
+ (target [evt] "Returns the target of the event")
+ (current-target [evt] "Returns the object that had the listener attached")
+ (event-type [evt] "Returns the type of the the event")
+ (raw-event [evt] "Returns the original GClosure event"))
+(def builtin-events (set (map keyword (gobj/getValues events/EventType))))
-;;;;;;;;;;;;;;;;;;; Event Wrappers ;;;;;;;;;;;;;;;;;
-
-(defn child-of?
- "returns true if the node(child) is a child of parent"
- [parent child]
- (cond
- (not child) false
- (identical? parent child) false
- (identical? (.-parentNode child) parent) true
- :else (recur parent (.-parentNode child))))
-
-(defn- mouse-enter-leave
- "this is used to build cross browser versions of :mouseenter and :mouseleave events"
- [func]
- (fn [e]
- (let [re (.-relatedTarget e)
- this (.-currentTarget e)]
- (when (and
- (not (identical? re this))
- (not (child-of? this re)))
- (func e)))))
-
-
-(defn- gen-wrapper
- "Generic event wrapper that handles listening and cleanup of wrapped events"
- [event-key wrapped-key wrapper-func]
- (let [obj (new js/Object)
- wevent (name wrapped-key)
- event (name event-key)]
- (set! (.-wrapped-event obj) wevent)
- (set! (.-event obj) event)
- (set! (.-listen obj)
- (fn [elm func capture opt-scope opt-handler]
- (let [callback (wrapper-func func)]
- (set! (.-listen callback) func)
- (set! (.-scope callback) opt-scope)
- (set! (.-event callback) event)
- (set! (.-capture callback) capture)
- (if op-handler
- (.listen opt-handler elm wevent callback capture)
- (events/listen elm wevent callback capture)))))
- (set! (.-unlisten obj)
- (fn [elm func capture opt-scope opt-handler]
- (let [listeners (if (= capture js/undefined)
- (concat (events/getListeners elm wevent false)
- (events/getListeners elm wevent true))
- (events/getListeners elm wevent capture))]
- (dorun
- (map (fn [obj]
- (let[listener (.-listener obj)
- lfunc (.-listen listener)
- scope (.-scope listener)
- capture (.-capture listener)]
- (when (and (or (not func) (= lfunc func))
- (or (not opt-scope) (= scope opt-scope)))
- (if opt-handler
- (.unlisten opt-handler elm wevent listener capture)
- (events/unlisten elm wevent listener capture))))) listeners)))))
- obj))
-
-(def wrapper-register (atom {}))
-
-;;;;;;;;;;;;;;;;;;; Public API ;;;;;;;;;;;;;;;;;
-
-(defn reg-event-wrapper! [event-key wrapped-key wrapper-func]
- (swap! wrapper-register assoc event-key (gen-wrapper event-key wrapped-key wrapper-func)))
+(def root-element (.. js/window -document -documentElement))
+(defn- find-builtin-type
+ [evt-type]
+ (if (contains? builtin-events evt-type)
+ (name evt-type)
+ evt-type))
+
+;; The listener function will always return true: we ignore the return value
+;; of the user-provided function since we don't want consumers to have to worry
+;; about that (user-provided functions are usually just a bunch of side effects)
+;; User functions can explicitly take control by calling prevent-default or
+;; stop-propegation
+(defn- create-listener-function
+ [f]
+ (fn [evt]
+ (f (reify
+ Event
+ (prevent-default [_] (.preventDefault evt))
+ (stop-propagation [_] (.stopPropagation evt))
+ (target [_] (.-target evt))
+ (current-target [_] (.-currentTarget evt))
+ (event-type [_] (.-type evt))
+ (raw-event [_] evt)
+ ILookup
+ (-lookup [o k]
+ (if-let [val (aget evt k)]
+ val
+ (aget evt (name k))))
+ (-lookup [o k not-found] (or (-lookup o k)
+ not-found))))
+ true))
+
+(defn- listen-internal!
+ [content type listener capture once]
+ (let [f (create-listener-function listener)
+ t (find-builtin-type type)]
+ (doall (for [node (domina/nodes content)]
+ (if once
+ (events/listenOnce node t f capture)
+ (events/listen node t f capture))))))
(defn listen!
- "adding an event to the selected nodes"
- ([nds event func] (listen! nds event func false))
- ([nds event func capture]
- (let [wrapper (@wrapper-register event)]
- (doseq [node (domina/nodes nds)]
- (if (nil? wrapper)
- (events/listen node (name event) func capture)
- (events/listenWithWrapper node wrapper func capture))))))
-
-(defn unlisten!
- "removing a specific event from the selected nodes"
- ([nds event func] (unlisten! nds event func false))
- ([nds event func capture]
- (let [wrapper (@wrapper-register event)]
- (doseq [node (domina/nodes nds)]
- (if (nil? wrapper)
- (events/unlisten node (name event) func capture)
- (.unlisten wrapper node func capture))))))
-
-(defn remove-listeners!
- "removes all listeners for a given set of events on the selected nodes"
- [nds & event-list]
- (doseq [node (domina/nodes nds)]
- (let [map-func #(let [wrapper (@wrapper-register %)]
- (if wrapper
- (.unlisten wrapper node)
- (events/removeAll node (name %))))]
- (doall (map map-func event-list)))))
-
-(defn fire-listeners!
- "fires the listeners attached to a set of nodes"
- [nds event capture event-map]
- (let [wrapper (@wrapper-register event)
- nevent (if wrapper (.-wrapped-event wrapper) (name event))
- event-obj (goog.events.Event. (event-map :type) (event-map :target))]
- (set! (.-relatedTarget event-obj) (event-map :related-target))
- (doseq [node (domina/nodes nds)]
- (events/fireListeners node nevent capture event-obj))))
-
-;;;;;;;;;;;;;;;;;;; Initialization ;;;;;;;;;;;;;;;;;
-(reg-event-wrapper! :mouseenter :mouseover mouse-enter-leave)
-(reg-event-wrapper! :mouseleave :mouseout mouse-enter-leave)
+ "Add an event listener to each node in a DomContent. Listens for events during the bubble phase. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element."
+ ([type listener] (listen! root-element type listener))
+ ([content type listener]
+ (listen-internal! content type listener false false)))
+
+(defn capture!
+ "Add an event listener to each node in a DomContent. Listens for events during the capture phase. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element."
+ ([type listener] (capture! root-element type listener))
+ ([content type listener]
+ (listen-internal! content type listener true false)))
+
+(defn listen-once!
+ "Add an event listener to each node in a DomContent. Listens for events during the bubble phase. De-registers the listener after the first time it is invoked. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element."
+ ([type listener] (listen-once! root-element type listener))
+ ([content type listener]
+ (listen-internal! content type listener false true)))
+
+(defn capture-once!
+ "Add an event listener to each node in a DomContent. Listens for events during the capture phase. De-registers the listener after the first time it is invoked. Returns a sequence of listener keys (one for each item in the content). If content is omitted, binds a listener to the document's root element."
+ ([type listener] (capture-once! root-element type listener))
+ ([content type listener]
+ (listen-internal! content type listener true true)))
+
+(defn unlisten!
+ "Removes event listeners from each node in the content. If a listener type is supplied, removes only listeners of that type. If content is omitted, it will remove listeners from the document's root element."
+ ([] (unlisten! root-element))
+ ([content]
+ (doseq [node (domina/nodes content)]
+ (events/removeAll node)))
+ ([content type]
+ (let [type (find-builtin-type type)]
+ (events/removeAll node type))))
+
+(defn- ancestor-nodes
+ "Returns a seq of a node and its ancestors, starting with the document root."
+ ([n] (ancestor-nodes n (cons n)))
+ ([n so-far]
+ (if-let [parent (.-parentNode n)]
+ (recur parent (cons parent so-far))
+ so-far)))
+
+;; See closure.goog.testing.events.fireBrowserEvent. This function will give us the same
+;; bubbling/capturing functionality as W3C DOM level 2 events, on native browser nodes,
+;; so we can dispatch against things that do not inherit from goog.event.EventTarget.
+(defn dispatch-browser!
+ "Intended for internal/testing use only. Clients should prefer dispatch!. Dispatches an event as a simulated browser event from the given source node. Emulates capture/bubble behavior. Returns false if any handlers called prevent-default, otherwise true."
+ [source evt]
+ (let [ancestors (ancestor-nodes (domina/single-node source))]
+ ;; Capture phase
+ (doseq [n ancestors]
+ (when-not (.-propagationStopped n)
+ (set! (.-currentTarget evt) n)
+ (events/fireListeners n (.-type evt) true evt)))
+ ;; Bubble phase
+ (doseq [n (reverse ancestors)]
+ (when-not (.-propagationStopped n)
+ (set! (.-currentTarget evt) n)
+ (events/fireListeners n (.-type evt) false evt)))
+ (.-returnValue_ evt)))
+
+(defn dispatch-event-target!
+ "Intended for internal/testing use only. Clients should prefer dispatch!. Dispatches an event using GClosure's event handling. The event source must extend goog.event.EventTarget"
+ [source evt]
+ (events/dispatchEvent source evt))
+
+(defn- is-event-target?
+ "Tests whether the object is a goog.event.EventTarget"
+ [o]
+ (and (.-getParentEventTarget o)
+ (.-dispatchEvent o)))
+
+(defn dispatch!
+ "Dispatches an event of the given type, adding the values in event map to the event object. Optionally takes an event source. If none is provided, dispatches on the document's root element. Returns false if any handlers called prevent-default, otherwise true."
+ ([type evt-map] (dispatch! root-element type evt-map))
+ ([source type evt-map]
+ (let [evt (events/Event. (find-builtin-type type))]
+ (doseq [[k v] evt-map] (aset evt k v))
+ (if (is-event-target? source)
+ (dispatch-event-target! source evt)
+ (dispatch-browser! source evt)))))
+
+(defn unlisten-by-key!
+ "Given a listener key, removes the listener."
+ [key]
+ (events/unlistenByKey key))
+
+(defn get-listeners
+ "Returns a sequence of event listeners for all the nodes in the
+content of a given type."
+ [content type]
+ (let [type (find-builtin-type type)]
+ (mapcat #(events/getListeners % type false) (domina/nodes content))))
View
312 test/cljs/domina/test.cljs
@@ -6,7 +6,10 @@
text set-text! value set-value! html set-html! set-data! get-data log-debug]]
[domina.xpath :only [xpath]]
[domina.css :only [sel]]
- [domina.events :only [listen! unlisten! remove-listeners! fire-listeners!]])
+ [domina.events :only [listen! capture! listen-once! capture-once!
+ unlisten! dispatch-event! dispatch! unlisten-by-key!
+ get-listeners prevent-default stop-propagation
+ target current-target event-type raw-event]])
(:require [goog.events :as events]
; [clojure.browser.repl :as repl]
))
@@ -48,7 +51,8 @@
(defn reset
"resets the page"
[]
- (destroy! (xpath "//body/*")))
+ (destroy! (xpath "//body/*"))
+ (unlisten! (xpath "//*")))
(defn standard-fixture
"Standard fixture html"
@@ -500,11 +504,10 @@
(assert (= "Some text." (text (xpath "//p") true)))))
;; Temporarily removed this test: IE8 handles this differently than other browsers.
-(comment
- (add-test "can retrieve the text value of a node without normalization."
- #(do (reset)
- (append! (xpath "//body") "<p>\n\n Some text. \n </p>")
- (assert (= "\n\n Some text. \n " (text (xpath "//p") false))))))
+(add-test "can retrieve the text value of a node without normalization."
+ #(do (reset)
+ (append! (xpath "//body") "<p>\n\n Some text. \n </p>")
+ (assert (= "\n\n Some text. \n " (text (xpath "//p") false)))))
(add-test "can set text on a single node"
#(do (reset)
@@ -550,12 +553,24 @@
(set-html! (xpath "//div") "<p class='foobar'>some text</p>")
(assert (= 1 (count (nodes (xpath "//body/div/p[@class='foobar']")))))))
+(add-test "can set a node's innerHTML to non-string content"
+ #(do (reset)
+ (append! (xpath "//body") "<div></div>")
+ (set-html! (xpath "//div") (nodes "<p class='foobar'>some text</p>"))
+ (assert (= 1 (count (nodes (xpath "//body/div/p[@class='foobar']")))))))
+
(add-test "can set multiple nodes' innerHTML"
#(do (reset)
(append! (xpath "//body") "<div></div><div></div>")
(set-html! (xpath "//div") "<p class='foobar'>some text</p>")
(assert (= 2 (count (nodes (xpath "//body/div/p[@class='foobar']")))))))
+(add-test "can set multiple nodes' innerHTML to non-string content"
+ #(do (reset)
+ (append! (xpath "//body") "<div></div><div></div>")
+ (set-html! (xpath "//div") (nodes "<p class='foobar'>some text</p>"))
+ (assert (= 2 (count (nodes (xpath "//body/div/p[@class='foobar']")))))))
+
(add-test "can set a table's innerHTML"
#(do (reset)
(append! (xpath "//body") "<table></table>")
@@ -589,99 +604,192 @@
(assert (= "THEAD" (. (first n) -tagName)))
(assert (= "TBODY" (. (second n) -tagName))))))
-(add-test "can trigger a handler on a :mouseover event"
- #(do (reset)
- (append! (xpath "//body") "<div id='ref'>Some content</div>")
- (listen! (by-id "ref") :mouseover (fn [] (append! (by-id "ref") "<p>Hello world!</p>")))
- (let [target (by-id "ref")]
- (fire-listeners! target :mouseover false {:type :mouseover :target target}))
- (assert (= "Hello world!" (text (xpath "//p"))))))
-
-(add-test "can trigger a handler on a :mouseout event"
- #(do (reset)
- (append! (xpath "//body") "<div id='ref'>Some content</div>")
- (listen! (by-id "ref") :mouseout (fn [] (append! (by-id "ref") "<p>Hello world!</p>")))
- (let [target (by-id "ref")]
- (fire-listeners! target :mouseout false {:type :mouseout :target target}))
- (assert (= "Hello world!" (text (xpath "//p"))))))
-
-(add-test "can trigger a handler on a :click event"
- #(do (reset)
- (append! (xpath "//body") "<div id='ref'>Some content</div>")
- (listen! (by-id "ref") :click (fn [] (append! (by-id "ref") "<p>Hello world!</p>")))
- (let [target (by-id "ref")]
- (fire-listeners! target :click false {:type :click :target target}))
- (assert (= "Hello world!" (text (xpath "//p"))))))
-
-
-(add-test "can trigger a handler on a :mouseenter event"
- #(do (reset)
- (append! (xpath "//body") "<div id='parent'><div id='ref'></div></div>")
- (listen! (by-id "ref") :mouseenter (fn [] (append! (by-id "ref") "<p>Hello world!</p>")))
- (let [rtarget (by-id "parent")
- target (by-id "ref")]
- (fire-listeners! target :mouseenter false {:type :mouseenter :related-target rtarget :target target}))
- (assert (= "Hello world!" (text (xpath "//p"))))))
-
-
-(add-test "can trigger a handler on a :mouseleave event"
- #(do (reset)
- (append! (xpath "//body") "<div id='parent'><div id='ref'></div></div>")
- (listen! (by-id "ref") :mouseleave (fn [] (append! (by-id "ref") "<p>Hello world!</p>")))
- (let [rtarget (by-id "parent")
- target (by-id "ref")]
- (fire-listeners! target :mouseleave false {:type :mouseleave :related-target rtarget :target target}))
- (assert (= "Hello world!" (text (xpath "//p"))))))
-
-(add-test "can remove-listeners on a :click event"
- #(let [handler (fn [] (append! (by-id "ref") "<p>Hello world!</p>"))]
- (reset)
- (append! (xpath "//body") "<div id='ref'>Some content</div>")
- (listen! (by-id "ref") :click handler)
- (remove-listeners! (by-id "ref") :click)
- (let [target (by-id "ref")]
- (fire-listeners! target :click false {:type :click :target target}))
- (assert (= "Some content" (text (xpath "//div"))))))
-
-(add-test "can remove-listeners on a :mouseenter event"
- #(let [handler (fn [] (append! (by-id "ref") "<p>Hello world!</p>"))]
- (reset)
- (append! (xpath "//body") "<div id='parent'><div id='ref'>Some content</div></div>")
- (listen! (by-id "ref") :mouseenter handler)
- (remove-listeners! (by-id "ref") :mouseenter)
- (let [rtarget (by-id "parent")
- target (by-id "ref")]
- (fire-listeners! target :mouseenter false {:type :mouseenter :related-target rtarget :target target}))
- (assert (= "Some content" (text (xpath "//div"))))))
-
-(add-test "can unlisten! on a :click event"
- #(let [handler (fn [] (append! (by-id "ref") "<p>Hello world!</p>"))]
- (reset)
- (append! (xpath "//body") "<div id='ref'>Some content</div>")
- (listen! (by-id "ref") :click handler)
- (unlisten! (by-id "ref") :click handler)
- (let [target (by-id "ref")]
- (fire-listeners! target :click false {:type :click :target target}))
- (assert (= "Some content" (text (xpath "//div"))))))
-
-(add-test "can unlisten! on a :mouseenter event"
- #(let [handler (fn [] (append! (by-id "ref") "<p>Hello world!</p>"))]
- (reset)
- (append! (xpath "//body") "<div id='parent'><div id='ref'>Some content</div></div>")
- (listen! (by-id "ref") :mouseenter handler)
- (unlisten! (by-id "ref") :mouseenter handler)
- (let [rtarget (by-id "parent")
- target (by-id "ref")]
- (fire-listeners! target :mouseenter false {:type :mouseenter :related-target rtarget :target target}))
- (assert (= "Some content" (text (xpath "//div"))))))
-
-(add-test "can append to a document fragment"
- #(do
- (reset)
- (let [frag (.createDocumentFragment js/document)]
- (append! frag "<div>testing</div>")
- (append! (xpath "//body") frag)
- (assert (= "testing" (text (xpath "//div")))))))
+;; Events
+
+(add-test "can add and retrieve a listener"
+ (fn []
+ (reset)
+ (append! (xpath "//body") "<button id='mybutton'>Text</button>")
+ (listen! (sel "#mybutton") :click (fn [e]
+ (reset! clicked true)))
+ (assert (= 1 (count (get-listeners (sel "#mybutton") :click))))))
+
+(defn simulate-click-event
+ "Doesn't use GClosure, to be more realistic"
+ [el]
+ (let [el (single-node el)
+ document (.-document js/window)]
+ (cond
+ (.-click el) (.click el)
+ (.-createEvent document) (let [e (.createEvent document "MouseEvents")]
+ (.initMouseEvent e "click" true true
+ js/window 0 0 0 0 0
+ false false false false 0 nil)
+ (.dispatchEvent el e))
+ :default (throw "Unable to simulate click event"))))
+
+(add-test "can listen for a browser event"
+ (fn []
+ (reset)
+ (append! (xpath "//body") "<button id='mybutton'>Text</button>")
+ (let [clicked (atom false)]
+ (listen! (sel "#mybutton") :click (fn [e]
+ (reset! clicked true)))
+ (simulate-click-event (sel "#mybutton"))
+ (assert @clicked))))
+
+(add-test "can extract string keys from an event using keyword accessors"
+ (fn []
+ (reset)
+ (append! (xpath "//body") "<button id='mybutton'>Text</button>")
+ (let [coords (atom nil)]
+ (listen! (sel "#mybutton") :foobar (fn [e]
+ (reset! coords
+ [(:clientX e)
+ (:clientY e)])))
+ (dispatch! (sel "#mybutton") :foobar {"clientX" 42
+ "clientY" 42})
+ (assert (= [42 42] @coords)))))
+
+
+(add-test "can dispatch an event, execute default action is true"
+ (fn []
+ (reset)
+ (append! (xpath "//body") "<button id='mybutton'>Text</button>")
+ (let [clicked (atom false)]
+ (listen! (sel "#mybutton") :click (fn [e]
+ (reset! clicked true)))
+ (let [default (dispatch! (sel "#mybutton") "click" {})]
+ (assert @clicked)
+ (assert default)))))
+
+(add-test "can prevent the default action on an event"
+ (fn []
+ (reset)
+ (append! (xpath "//body") "<button id='mybutton'>Text</button>")
+ (listen! (sel "#mybutton") :click (fn [e]
+ (prevent-default e)))
+ (assert (not (dispatch! (sel "#mybutton") "click" {})))))
+
+(add-test "capture and bubble listeners are triggered in the correct order."
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom [])]
+ (listen! (sel "body") :click #(swap! clicked conj :listened))
+ (capture! (sel "body") :click #(swap! clicked conj :captured))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= [:captured :listened] @clicked)))))
+
+(add-test "current-target is correct when capturing custom events"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [actual-elements (atom [])
+ body (domina/single-node (sel "body"))
+ button (domina/single-node (sel "button"))]
+ (listen! (sel "body") :foobar #(swap! actual-elements conj
+ (current-target %)))
+ (listen! (sel "button") :foobar #(swap! actual-elements conj
+ (current-target %)))
+ (dispatch! (sel "#mybutton") :foobar {:some "data"})
+ (assert (= [button body] @actual-elements)))))
+
+(add-test "can stop event propagation in the capture phase."
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom false)]
+ (capture! (sel "div") :click #(stop-propagation %))
+ (listen! (sel "#mybutton") :click #(reset! clicked true))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (not @clicked)))))
+
+(add-test "can stop event propagation in the bubble phase."
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom false)]
+ (listen! (sel "body") :click (fn [e]
+ (reset! clicked true)))
+ (listen! (sel "#mybutton") :click (fn [e]
+ (stop-propagation e)))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (not @clicked)))))
+
+(add-test "listen-once triggers only once"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom 0)]
+ (listen-once! (sel "body") :click #(swap! clicked inc))
+ (simulate-click-event (sel "#mybutton"))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= 1 @clicked)))))
+
+(add-test "listen-once triggers only once"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom 0)]
+ (listen-once! (sel "body") :click #(swap! clicked inc))
+ (simulate-click-event (sel "#mybutton"))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= 1 @clicked)))))
+
+(add-test "can unlisten generically"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom 0)]
+ (listen! (sel "body") :click #(swap! clicked inc))
+ (simulate-click-event (sel "#mybutton"))
+ (unlisten! (sel "body"))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= 1 @clicked)))))
+
+(add-test "can unlisten narrowed by type"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom 0)]
+ (listen! (sel "body") :click #(swap! clicked inc))
+ (simulate-click-event (sel "#mybutton"))
+ (unlisten! (sel "body") :foobar)
+ (simulate-click-event (sel "#mybutton"))
+ (unlisten! (sel "body") :click)
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= 2 @clicked)))))
+
+(add-test "can unlisten by key"
+ (fn []
+ (reset)
+ (append! (xpath "//body")
+ "<div><button id='mybutton'>Text</button></div>")
+ (let [clicked (atom 0)
+ keys (listen! (sel "body") :click #(swap! clicked inc))]
+ (simulate-click-event (sel "#mybutton"))
+ (unlisten-by-key! (first keys))
+ (simulate-click-event (sel "#mybutton"))
+ (assert (= 1 @clicked)))))
+
+(add-test "can send and listen for custom events with custom data"
+ (fn []
+ (reset)
+ (let [worked (atom false)]
+ (listen! :foobar (fn [evt]
+ (if (= "data" (:some evt))
+ (reset! worked true))))
+ (dispatch! :foobar {:some "data"})
+ (assert @worked))))
(add-test "doesn't clone unless necessary"
#(do
@@ -690,7 +798,6 @@
(append! (xpath "//body") child)
(assert (= child (single-node (xpath "//body/div")))))))
-
(add-test "test that data works"
#(do
(reset)
@@ -702,7 +809,8 @@
(add-test "test that data works with bubbling"
#(do
(reset)
- (append! (xpath "//body") "<div id='outerdata'><div id='innerdata'>hello</div></div>")
+ (append! (xpath "//body")
+ "<div id='outerdata'><div id='innerdata'>hello</div></div>")
(let [data {:some "complex data"}]
(set-data! (by-id "outerdata") :my-impeccable-data data)
(assert (= data (get-data (by-id "innerdata") :my-impeccable-data true))))))

No commit comments for this range

Something went wrong with that request. Please try again.