From 23601e779af76801ec9963950159eb1d3426f7b2 Mon Sep 17 00:00:00 2001 From: David Frese Date: Tue, 18 Jun 2024 15:45:45 +0200 Subject: [PATCH] Add transient-form, subscription-form and ajax/form. --- src/reacl_c_basics/ajax.cljs | 25 +++++++++++ src/reacl_c_basics/forms/core.cljs | 69 ++++++++++++++++++++++++++++++ test/reacl_c_basics/ajax_test.cljs | 29 +++++++++++++ 3 files changed, 123 insertions(+) diff --git a/src/reacl_c_basics/ajax.cljs b/src/reacl_c_basics/ajax.cljs index 7ed3d3a..c326079 100644 --- a/src/reacl_c_basics/ajax.cljs +++ b/src/reacl_c_basics/ajax.cljs @@ -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)) diff --git a/src/reacl_c_basics/forms/core.cljs b/src/reacl_c_basics/forms/core.cljs index d422e23..bcc280a 100644 --- a/src/reacl_c_basics/forms/core.cljs +++ b/src/reacl_c_basics/forms/core.cljs @@ -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 diff --git a/test/reacl_c_basics/ajax_test.cljs b/test/reacl_c_basics/ajax_test.cljs index c5f0453..c1d0b5f 100644 --- a/test/reacl_c_basics/ajax_test.cljs +++ b/test/reacl_c_basics/ajax_test.cljs @@ -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])) @@ -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))))))))