Skip to content

Commit

Permalink
Add transient-form, subscription-form and ajax/form.
Browse files Browse the repository at this point in the history
  • Loading branch information
David Frese committed Jun 18, 2024
1 parent cfd82fd commit 23601e7
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/reacl_c_basics/ajax.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,28 @@ given request and deliver the response as soon as it is available."
(delivery item {:transition transition
:manage manage}))))

(dom/defn-dom form
"A form to allow the user to edit the state and submit it using an Ajax request.
The state of `content` is a copy of the form state until a form
submit (or reset) happens. On submit, the Ajax request `((:request
attrs) state)` is executed. If that is successful (`((:successful?
attrs) response)`, which defaults to [[response-ok?]]), the form
state is updated.
Set `(:emit-result? attrs)` to add additional handling of the
result, like using a value returned from the server as the new
state, or to set an error flag.
The form is automatically disabled while the request is running.
Use HTML5 form validation (and [[reacl-c-basics.forms.core/input]]
to prevent submitting invalid states."
[attrs & content]
(assert (some? (:request attrs)) "Missing :request attribute.")
(apply forms/subscription-form
(dom/merge-attributes attrs
{:subscription (f/comp execute (:request attrs))
:emit-result? (:emit-result? attrs)
:successful? (or (:successful? attrs) response-ok?)})
content))
69 changes: 69 additions & 0 deletions src/reacl_c_basics/forms/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,75 @@
[attrs & content]
(apply with-invalid-attr dom/form attrs content))

(let [t-submit (fn [[external internal] ev]
(.preventDefault ev)
[internal internal])
t-reset (fn [[external internal] ev]
(.preventDefault ev)
[external external])]
(dom/defn-dom transient-form
"Same as [[form]], but the state of `content` is a copy of the form state,
and form submit and form reset publish or discard the modified state
respectively."
[attrs & content]
(c/with-state-as external
(c/with-state-as [_ internal :local external]
(form (dom/merge-attributes attrs
{:onSubmit t-submit
:onReset t-reset})
(c/focus lens/second
(apply c/fragment content)))))))

(let [t-submit (fn [[external local] ev]
(.preventDefault ev)
[external (assoc local :submit true)])
t-reset (fn [[external local] ev]
(.preventDefault ev)
[external (assoc local :internal external)])]
(dom/defn-dom subscription-form
"Same as [[transient-form]] but on form submit the
subscription `((:subscription attrs) state)` is used, which should emit the
result of an asynchronous operation.
If that result action denotes a successful operation
(using `((:successful? attrs) result)`, which defaults to constantly
true), then the form state is set to the state submitted.
If `(:emit-result? attrs)` is true, then the result action is
re-emitted from the form. That allows to reset the state to a value
contained in the result, to set an external error flag, or to close a
modal in which the form is shown.
The form is disabled automatically while the asynchronous operation is
in progress.
See [[reacl-c-basics.ajax/form]] for a version of this that is
specialized to submitting values via ajax."
[attrs & content]
(let [submit (:subscription attrs)
successful? (or (:successful? attrs) (constantly true))
emit-result? (:emit-result? attrs)]
(assert (some? submit) "Missing :submit attribute.")
(c/with-state-as external
(c/with-state-as [_ local :local {:internal external :subscription false}]
(form (dom/merge-attributes (dissoc attrs :submit :successful? :emit-result?)
{:onSubmit t-submit
:onReset t-reset})
(c/focus (lens/>> lens/second :internal)
(dom/fieldset {:disabled (:submit local)}
(apply c/fragment content)))
(when (:submit local)
(-> (submit (:internal local))
(c/handle-action (fn [[external local] result]
(cond-> (c/return :state [(if (successful? result)
;; internal -> external
(:internal local)
;; else keep external
external)
(assoc local :submit false)])
emit-result? (c/merge-returned (c/return :action result)))))))))))))


(defn- lift-type [t]
(if (type? t)
t
Expand Down
29 changes: 29 additions & 0 deletions test/reacl_c_basics/ajax_test.cljs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
(ns reacl-c-basics.ajax-test
(:require [reacl-c-basics.ajax :as ajax]
[reacl-c-basics.ajax-test-util :as ajax-test-util]
[reacl-c.core :as c :include-macros true]
[reacl-c.dom :as dom]
[reacl-c.test-util.core :as tuc :include-macros true]
[reacl-c-basics.jobs.core :as jobs]
[reacl-c.test-util.dom-testing :as dt]
[reacl-c-basics.forms.core :as forms]
[active.clojure.functions :as f]
[active.clojure.lens :as lens]
[cljs.test :refer (is deftest testing async) :include-macros true]))
Expand Down Expand Up @@ -146,3 +148,30 @@
(fn [res]
(is (ajax/response? res))
(done))))))))

(deftest form-test
(let [post (fn [v] (ajax/POST "http://invalid.invalid/" {:params v}))]
(dt/rendering
(-> (ajax/form {:data-testid "form"
:request post}
(c/focus :foo (forms/input {:data-testid "input" :type "text"}))
(dom/input {:data-testid "submit" :type "submit"}))
(ajax-test-util/emulate-requests
{(post {:foo "baz"}) (ajax/make-response true :ok)}))
:state {:foo "bar"}
(fn [env]
(let [input (dt/get env (dt/by-testid "input"))
submit-btn (dt/get env (dt/by-testid "submit"))]

;; change value to baz
(dt/fire-event input
:change {:target {:value "baz"}})

;; not published yet
(is (= {:foo "bar"} (dt/current-state env)))

;; submit (with ok response)
(dt/fire-event submit-btn :click)

;; state changed
(is (= {:foo "baz"} (dt/current-state env))))))))

0 comments on commit 23601e7

Please sign in to comment.