This project is in review state. Please check it out and leave comments.
A ClojureScript library that is part of the bones project. It helps to keep reagent form components concise when using re-frame. Here is a demo: https://youtu.be/dGQRDSRACB4
The core concept of this library is a standardized event vector that maps to a path in the app-db, e.g.:
(get-in app-db path)
An event can be dispatched with this path to get data into the db and a subscription can be registered with this path to get data out. This path holds things that are meant to be edited. These edits are meant to be sent to a server and persisted which entails a lifecycle of events. The values and meta data about these edits are stored in the db, which looks like this:
{:editable {:typeX {"id-of-X" {:inputs {}
:errors {}
:state {}}
;; optional
:_meta {:spec ::typeX
:defaults {}}}}}
:inputs
are the form values. This is the form state stored in the app-db.
Basically the theory is that a human moves very slow so their inputs, even when
typing, do not pose a performance concern. The goal of this project is to
support the majority of user inputs as much as possible and I believe that this
works quite well.
:errors
can be populated when inputs don't conform to a spec, or when the
server responds with an error code. It is then up to the component to subscribe
to these errors and render them to the user.
:state
It would be nice to generalize the states that forms go through and provide
a customizable state machine. That hasn't been done yet though, so this key only
holds the keyword :pending during a request. This can be used to show a spinner.
:_meta
Keeping the descriptions of the things next to the things can be nice.
If you provide a clojure.spec under :spec
, the inputs will be conformed before
sending them to the server and spec problems will show up in :errors
.
:defaults
can be set to send along with form submissions, see below.
We can use the path mentioned above in closure functions, which look pretty in hiccup. Here is what an "editable" form looks like with these closures:
(let [{:keys [reset save errors]} (e/form :login :new)]
[:span.errors (errors :username)]
[e/input :login :new :username :class "form-control"] ;;=> <input class="form-control"...
[:button {:on-click save}] ;;=> submit to server
)
Nice and tidy right? Here is what the component would look like without the closures. This also shows the paths that were mentioned above.
(let [inputs (subscribe [:editable :login :new :inputs])
errors (subscribe [:editable :login :new :errors])]
[:span.errors (:username @errors)]
[:input :class "form-control"
:value (:username @inputs)
:on-change #(dispatch-sync [:editable :login :new :inputs :username ( -> % .-target .-value)])]
[:button {:on-click #(dispatch [:request/command :login :new {:args @inputs}])}])
The :login
key here is serving as the type and the :new
key is serving as an
identifier. Here, the identifier can be a simple symbol because there is only
one login form, but other types could have many identifiers.
This project does not have an HTTP client. A client must be configured, or plugged in. (see: bones.client) This is done with a protocol and a reframe-cofx handler:
;; Extend the Client protocol like so:
(extend-type other/Client
e/Client
(e/login [cmp args tap]
(other/login cmp args tap))
(e/logout [cmp tap]
(other/logout cmp tap))
(e/command [cmp cmd args tap]
(other/command cmp cmd args tap))
(e/query [cmp args tap]
(other/query cmp args tap)))
;; then on initialization of the app, set the cofx handler:
(e/set-client (other/Client. some-config))
Response handlers are a very important part of an application and can often hold
a lot of system logic that is unique to the application. This isn't business
logic but system logic. Ideally this would be abstracted away so that even form
submission and success or errors could be a matter of whether there is data
present in the app-db. We are not there yet though, and I'm sure you will need
to customize the system logic that runs from a server response. That is why they
have been implemented as a multi-method. This means you can override any
response per status code that you need to. Mostly I think it will be
:response/command 200
and :event/message
. These are the two events that
occur from a positive response from the server. :event/message
is a message
from a WebSocket connection or SSE channel. The other responses will result
in data being created in the app-db under the :error
key in the editable
thing, which can be subscribed to and rendered.
(defmethod e/handler [:response/command 200]
[{:keys [db]} [_ response-body]]
(dispatch [:hurray :success response-body]))
(defmethod e/handler :event/message
[{:keys [db]} [channel message]]
(dispatch [:wow :another message])))
The :request/command
event handler is loaded automatically. It is a very flexible
way to send data to the server outside of the forms.
Here is a dispatch that will send only the value of the :args
option:
(dispatch [:request/command :command/name nil {:args {:abc 123}}])
Here is a dispatch that will pull data from the db, which is the form values for the editable thing with type :abc and id 123:
(dispatch [:request/command :abc 123])
Here is a dispatch that will merge the form inputs into some defaults (defaults are
in the db under the path: [:editable :abc :_meta :defaults]
):
(dispatch [:request/command :abc 123 {:merge [:inputs :defaults]}])
You may wan to send a computed value along with form inputs and defaults. To do this use the merge option like so:
(dispatch [:request/command :abc 123 {:args {:xyz (+ 4 5)}
:merge [:inputs :defaults]}])
```
## Testing in the browser
Install karma and headless chrome
```
npm install -g karma-cli
npm install karma karma-cljs-test karma-chrome-launcher --save-dev
```
And then run your tests
```
lein clean
lein doo chrome-headless test once
```
## License
Copyright © 2016 teaforthecat
Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close