Yet another Form Library to add to the list. Inspired by Formik.
For Reagent & Re-frame
If there is anything certain about us developers, it is that sooner or later we will have to deal with forms. No way around it.
Fork tries to establish a pattern by abstracting away the bits you have written over and over to shift your focus on the features implementation. The following points represent the pillars Fork was built upon:
As at this state you must be dying of curiosity, I will dive right into the code implementation hoping that this will help you save the day... and some nerves.
fork {:mvn/version "1.2.5"}
or
fork {:git/url "https://github.com/luciodale/fork.git"
:sha "sha commit"}
(ns your.namespace
(:require
[fork.core :as fork]))
(defn my-form
[{:keys [values handle-change handle-blur]}]
[:div
[:p "Read back: " (values "input")]
[:input
{:name "input"
:value (values "input")
:on-change handle-change
:on-blur handle-blur}]])
(defn foo []
[fork/form {:initial-values
{"input" "hello"}}
my-form])
Notice that Fork takes only two parameters. The first one is a map of utilities you provide, and the second one is a function that returns your component. It is by destructuring the first and only param of the form function that you get all the goodies straight from the API.
Starting from :initial-values
, this key might be provided to make Fork aware of any of your prefilled form values. Make sure to match the :name
of your inputs with what you define in the :initial-values
map to successfully link up the handlers. Do not use keywords for input names, as html casts them to strings anyways giving you ":input"
. If you don't need to set default values for your fields, you can discard this key.
You can also return your component in an anonymous function, but you have to be careful not to cause unwanted re-renderings by updating any external state. The following code is an example of what you want to avoid, when using an anonymous function:
;; Bad code
(defn foo []
(let [external-input (r/atom nil)]
(fn []
[:div
[:input
{:value @external-input
:on-change #(reset! external-input
(-> % .-target .-value))}]
[fork/form {}
(fn [{:keys [values
form-id
handle-change
handle-blur] :as props}]
[:form
{:id form-id}
[:input
{:name "name"
:value (values "name")
:on-change #(do (reset! external-input "some-value")
(handle-change %))
:on-blur handle-blur}]])]])))
Briefly, the "name"
input will lose focus every time its :on-change
event is dispatched. This happens because the handler creates a new value for the external-input
state, which sparks the re-rendering of the whole foo
component.
As a solution, you might keep the anonymous function in place as long as you remember to use the foo
component exclusively for the logic related to fork/form
. This approach will be adopted in the next snippets for readability purposes.
(ns your.namespace
(:require
[fork.core :as fork]
[re-frame.core :as rf))
(rf/reg-event-fx
:submit-handler
[(fork/on-submit :form)]
(fn [{db :db} [_ {:keys [values]}]]
(js/alert values)
{:db (fork/set-submitting db :form false)}))
(defn foo []
[fork/form {:path :form
:form-id "id"
:prevent-default? true
:clean-on-unmount? true
:on-submit #(rf/dispatch [:submit-handler %])}
(fn [{:keys [values
form-id
handle-change
handle-blur
submitting?
handle-submit]}]
[:form
{:id form-id
:on-submit handle-submit}
[:input
{:name "input"
:value (values "input")
:on-change handle-change
:on-blur handle-blur}]
[:button
{:type "submit"
:disabled submitting?}
"Submit Form"]])])
Let's examine what has been added step by step:
:path
and other optionshandle-submit
and submitting?
to be used in your UIIf some parts look a bit obscure, the following detailed explanation will get rid of all your doubts.
:form-id
makes fork aware of your form elements. If it is not specified, a random id will be generated and will be provided through the same :form-id
key. It is mandatory to use it
:path
lets you choose where to store your form global state in Re-frame
:prevent-default?
does not automatically submit your form to the server
:clean-on-unmount?
resets the global state when your component is unmounted
:validation
to pass a validation function that takes the values as only param
:initial-values
to pre-populate the inputs
:initial-touched
to pre-populate the inputs and set them as touched
:on-submit
lets you write your own submit logic in a Re-frame event
:on-submit-response
to provide a map of server messages based on status codes
:component-did-mount
to perform any logic after the component is mounted. It takes a function and provides one argument that consists of a map of handlers: set-touched, set-untouched, set-values, disable, enable, disabled?, handle-change, handle-blur, send-server-request
After clicking the submit button, the interceptor (fork/on-submit :form)
sets submitting?
to true. Remember to pass :form
to the interceptor function, and make sure that it matches the :path
value you have given to Fork. At this stage, your event is executed and the only detail to remember is to set :submitting?
to false when the form life cycle is completed. You can choose to handle the global state with your own functions or rely on some helpers like fork/set-submitting
. It's really up to you.
You probably want to know more than the same old Hello World demonstration. Hence, I have prepared a REAL example that includes a server request and shows better what Fork can do for you.
(ns your.namespace
(:require
[ajax.core :as ajax]
[day8.re-frame.http-fx]
[fork.core :as fork]
[re-frame.core :as rf))
(rf/reg-event-fx
:success
[(fork/clean :form)]
(fn [{db :db} [_ result]]
{:db (assoc db :result result)}))
(rf/reg-event-fx
:failure
(fn [{db :db} [_ result]]
{:db (-> db
(fork/set-submitting :form false)
(fork/set-status-code :form 500))}))
(rf/reg-event-fx
:submit-handler
[(fork/on-submit :form)]
(fn [{db :db} [_ {:keys [values]}]]
{:db db
:http-xhrio
{:method :post
:uri "/submit-form"
:params values
:timeout 2000
:format (ajax/transit-request-format)
:response-format (ajax/transit-response-format)
:on-success [:success]
:on-failure [:failure]}}))
(defn foo []
[fork/form {:form-id "id"
:path :form
:prevent-default? true
:clean-on-unmount? true
:on-submit-response {400 "client error"
500 "server error"}
:on-submit #(rf/dispatch [:submit-handler %])}
(fn [{:keys [values
form-id
handle-change
handle-blur
submitting?
on-submit-response
handle-submit]}]
[:form
{:id form-id
:on-submit handle-submit}
[:input
{:name "input"
:value (values "input")
:on-change handle-change
:on-blur handle-blur}]
[:button
{:type "submit"
:disabled submitting?}
"Submit Form"]
[:p on-submit-response]])])
A few things to keep in mind:
Always return the db in your :submit-handler
to not lose the interceptor updates i.e. {:db db}
Notice the usage of fork/set-status-code
in the :failure
handler. It makes sure that on-submit-response
will get the right message out of the :on-submit-response
map passed to Fork
You might choose to use the fork/clean
interceptor to clean the whole state or parts of it i.e. (fork/clean :form :submitting?)
You don't really need to clean the state if your component is unmounted, as the :clean-on-unmount?
option will take care of it
Simply plug in any library of your choice that is side effect free, or build your custom validation.
All you have to do is to pass a function that takes values
as only parameter. The returned data will be accessible via the key errors
, which can be destructured from the props.
Let's now build some real validation for our Fork component using for example the Vlad library:
(def validation
(vlad/join (vlad/attr ["name"]
(vlad/chain
(vlad/present)
(vlad/length-in 3 15)))
(vlad/attr ["password"]
(vlad/chain
(vlad/present)
(vlad/length-over 7)))))
Let's integrate the validation with our Fork component to actually display the errors:
[fork/form {:path :form
:form-id "id"
:validation #(vlad/field-errors validation %)
:prevent-default? true
:clean-on-unmount? true
:on-submit #(rf/dispatch [:submit-handler %])}
(fn [{:keys [values
form-id
errors
touched
handle-change
handle-blur
submitting?
handle-submit]}]
[:form
{:id form-id
:on-submit handle-submit}
[:input
{:name "name"
:value (values "name")
:on-change handle-change
:on-blur handle-blur}]
(when (touched "name")
[:div (first (get errors (list "name")))])
[:input
{:name "password"
:value (values "password")
:on-change handle-change
:on-blur handle-blur}]
(when (touched "password")
[:div (first (get errors (list "password")))])
[:button
{:type "submit"
:disabled submitting?}
"Submit Form"]])]
Noticed anything new? We are simply passing the vlad validation function along with a :validation
key and destructuring touched
. The latter comes in handy to improve the user experience in that the errors are not shown until the first :on-blur
event is fired.
When a validation function is provided, the submit button will do nothing until all errors are cleared. The only variable that does change is submit-count
, which is incremented every time the on-click
event is fired.
To perform password
and confirm-password
validation I recommend using the helper vlad/equals-value
, as this really simplifies your logic. Briefly, you can define your validation like the following snippet:
(def form-validation
(fn [password]
(vlad/join
(vlad/attr ["password"]
(vlad/chain (vlad/present)
(vlad/join (vlad/length-in 6 128))))
(vlad/attr ["confirm-password"]
(vlad/chain
(vlad/equals-value
password
{:message "Confirm Password must be same as password"}))))))
and pass the password value when giving the function to Fork i.e.
[fork/form {:validation
#(vlad/field-errors
;; passing password to the function
(form-validation (get % "password"))
%)
...}
...]
Since version 1.1.0
, the handler send-server-request
provides a way of performing server side validation :on-blur
or :on-change
, or any other operation that involves your backend code. Here is an example of how it works:
(rf/reg-event-fx
:server-request
(fn [_ [_ values]]
;; faking a server request
{:dispatch-later [{:ms 200 :dispatch [:response values]}]}))
(rf/reg-event-fx
:response
(fn [{db :db} [_ values]]
;; so that the form can be submitted
{:db (fork/set-waiting db :form "email" false)}))
(defn foo []
[fork/form {:path :form
:prevent-default? true
:on-submit #(js/alert %)}
(fn [{:keys [form-id
values
handle-change
handle-blur
handle-submit
send-server-request]}]
[:div
[:form
{:id form-id
:on-submit handle-submit}
[:input
{:name "email"
:value (values "email")
:on-blur handle-blur
:on-change (fn [evt]
(handle-change evt)
(send-server-request evt
#(rf/dispatch [:server-request %])
;; optional
{:debounce 500}))}]
[:button
{:type "submit"}
"Submit"]]])])
After destructuring send-server-request
, this function is invoked within the :on-change
handler. It takes either two or three parameters being:
An event - Required
A function that performs the server request, taking the up to date values as argument - Required
An optional map - :debounce
and :throttle
are both supported from v1.2.4
To prevent the form submission while waiting for a server response, a :waiting? true
key value pair is stored in the Re-frame state and needs to be set to false after the server logic is resolved. You can do this yourself or use (fork/set-waiting db :form "email" false)
, as showed above. Now, the form can be submitted.
You bet it does. The keys you can currently access from your form function are:
[{:keys
[db
props
state
reset
values
form-id
errors
touched
set-touched
set-untouched
submitting?
submit-count
set-values
disable
enable
disabled?
handle-change
handle-blur
handle-submit
on-submit-response
send-server-request]}]
Here is a demonstration on how to use the above handlers that have not been mentioned so far:
;; db is simply the dereferenced re-frame state that fork uses for global logic
(swap! state assoc :something :new)
;; to remove full state
(reset)
;; to reset state to given map
(reset {:values {"name" "John"}
:touched #{"name"}})
(set-touched "input" "another-input")
(set-untouched "input" "another-input")
(set-values {"input" "new-value"})
(set-values {"input" "new-value"
"another-input" "new-value-too"})
(disable "input")
(disable "input" "another-input")
(enable "input")
(enable "input" "another-input")
;; input component
[:input
{...
:disabled (disabled? "input")
...}]
For what concerns the :props
key, you can use it as a way of passing arguments to the form component. Here is a quick example:
(defn my-form
[{:keys [props ...]}]
;; props accessible in here!
...
)
(defn foo []
[fork/form {:props {:arg1 "foo" :arg2 "bar"}
...}
my-form])
Certainly not, Fork gives you some pre-built inputs, yet you are condemning yourself to the Bulma CSS framework if you ever choose to go down that route. Creating your own wrappers would work much better, allowing you to retain full control on the style. Having said that, these are the few components that ship with Fork.
The quickest way to get Bulma is to require the CSS in the header of your index.html file:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css"/>
[fork/input props
{:name "input"
:label "First Name"
:placeholder "Joe"
:type "text"
:class "your-css-class"}]
To get all the props from your form function in one shot, you can add :as props just like this: {:keys [values ..] :as props}
[fork/textarea props
{:name "area"
:label "Summary"
:placeholder "Max 400 words"
:type "text"
:class "your-css-class"}]
[fork/checkbox props
{:name "agree"
:text "Plain text or component as well"}]
If you pass a component to :text
such as [:div "Some text"]
, add the display: inline;
style to the element.
[fork/pretty-dropdown props
{:label "Optional Label"
:name "pretty-dropdown"
:options [{"key-1" 1}
{"key-2" 2}
{"key-3" 3}]
:class "Optional Css Class"}]
[fork/dropdown props
{:label "Optional Label"
:name "pretty-dropdown"
:options [{"key-1" 1}
{"key-2" 2}
{"key-3" 3}]
:class "Optional Css Class"}]
Yes you can. This documentation should get you started with Fork the right way so that you can be productive with it. Ideas, comments (good or bad), and suggestions are always welcome!
Can you improve this documentation? These fine people already did:
Lucio D'Alessandro & lucioEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close