Reforms
A Clojurescript library that lets you build beautiful data-binding forms with Om, Reagent and Rum.
You can write code that is fully portable between Reagent, Om and Rum making it easier to reuse code and giving you a clear migration path.
To help you quickly create beautiful forms without messing with CSS, the generated markup is compatible with Bootstrap 3 CSS and Font Awesome. For quick results simply include Bootstrap and Font Awesome CSS.
If you think something useful is missing though, please let me know.
A good place to see the available controls: demo.
- Usage
- Getting started with Om
- Getting started with Reagent
- Getting started with Rum
- External CSS
- Quick tutorial
- Prettying it up
- Validation
- Tables
- Assorted topics
- Demos
- FAQ
- [API Reference](#api-referencehttpbilusgithubioreformsdoc)
- TBD
- Credits
- License
Usage
Getting started with Om
Add om-reforms
to :dependencies
in project.clj:
Minimal requires (including sablono to render the forms):
(ns hello-world.core
(:require [reforms.om :include-macros true :as f]
[om.core :as om]
[sablono.core :include-macros true :as sablono]))
Here's how you create an Om component with a form with just one text field and a button:
(defn simple-view
[data _owner]
(om/component
(sablono/html
(f/form
(f/text "Your name" data [:name])
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data))))))))
You render it with om/build
just like any other component. See https://github.com/omcljs/om for more details.
Note that labels are optional, you can render controls without labels, for instance:
(f/text data [:name] :placeholder "Enter your name here")
Getting started with Reagent
Add reagent-reforms
to :dependencies
in project.clj:
(ns hello-world.core
(:require [reforms.reagent :include-macros true :as f]
[reagent.core :refer [atom render-component]))
Here's how you create a Reagent component with a form with just one text field and a button:
(defn simple-view
[data]
(f/form
(f/text "Your name" data [:name])
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data))))))
You render it just like any other component by either mounting it using render-component
or inside another component using the [simple-view some-data]
syntax. See https://github.com/reagent-project/reagent for more details.
Note that labels are optional, you can render controls without labels, for instance:
(f/text data [:name] :placeholder "Enter your name here")
Getting started with Rum
Add rum-reforms
to :dependencies
in project.clj:
(ns hello-world.core
(:require [reforms.rum :include-macros true :as f]
[rum.core :include-macros true :as rum])
Here's how you create a Rum component with a form with just one text field and a button:
(rum/defc simple-view < rum/cursored rum/cursored-watch [data horizontal-orientation]
[data]
(f/form
(f/text "Your name" data [:name])
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data))))))
You render it just like any other component by either mounting it using rum-mount
or inside another component. See https://github.com/tonsky/rum for more details.
Note that labels are optional, you can render controls without labels, for instance:
(f/text data [:name] :placeholder "Enter your name here")
External CSS
The library does not use Bootstrap JavaScript so just link to bootstrap css from your html page, e.g.:
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet"/>
Optionally, to use Font Awesome icons to use features such as progress spinner, warning icons etc., link to it as well:
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet">
Quick tutorial
The tutorial shows library-agnostic code. For code specific to Om or React, see "Getting started with ..." above or the examples.
Hello, world!
Here's how you create a form with just one text field and a button:
(f/form
(f/text "Your name" data [:name])
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data)))))
Note that form
returns a Hiccup-like data structure. The example below, though a bit simplified and scrubbed for clarity, should give you an idea:
[:form [:div {:class "form-group"
:key "data-name"}
[:label {:for "data-name"
:class "control-label "} "Your name"]
[:input {:value "My name"
:type "text"
:class "form-control"
:id "data-name"
:placeholder "Type your name here"}]]
[:div.form-group.form-buttons
[:button {:type "button"
:class "btn btn-primary"
:onClick #(js/alert (:name @data))} "Submit"]]]
Data binding
The controls bind directly to data (Om cursors or Reagent ratoms). For example, as the user types text into the text box below, data
is automatically updated:
(f/text "Your name" data [:name])
(prn @data) ;; => {:name "John Wayne}
Prettying it up
Adding a placeholder
You can add a placeholder shown when the text box is empty using a :placeholder
option:
(f/text "Your name" data [:name] :placeholder "Enter your name here")
Changing orientation
To change the orientation use with-options
:
(f/with-options {:form {:horizontal true}}
(f/form
(f/text "Your name" data [:name] :placeholder "Enter your name here")
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data))))))
Wrapping in a panel
To wrap the form in a panel use panel
:
(f/panel
"Hello, world"
(f/form
(f/text "Your name" data [:name] :placeholder "Enter your name here")
(f/form-buttons
(f/button "Submit" #(js/alert (:name @data))))))
Button types
Finally, let's take make the button clearly a primary one and add a cancel button and, just for the fun of it, a checkbox that toggles the orientation:
(f/form
(f/text "Your name" data [:name] :placeholder "Enter your name here")
(f/form-buttons
(f/button-primary "Submit" #(js/alert (:name @data)))
(f/button-default "Cancel" #(js/alert "Cancel!")))
(f/checkbox "Horizontal form" data [:orientation-horizontal]))
Click!
The complete example: Om (demo) Reagent (demo).
For the list of available controls, see the API Reference.
Validation
The library supports client-side data validation.
Basics
To use validators, require
reforms.validation
, use form and form field helpers from this namespace instead of reforms.core
and use validate!
:
(ns my-validation-example
(:require ...
[reforms.validation :include-macros true :as v]))
Apart from form
, the helpers have an identical interface to ones in reforms.core
.
(v/form ;; 1
ui-state ;; 2
(v/text "Login" data [:login]) ;; 3
(v/password "Password" data [:password1])
(v/password "Confirm password" data [:password2])
(f/form-buttons
(f/button-primary "Sign up" #(sign-up! data ui-state)))) ;; 4
- We use
reforms.validation/form
. Note that it takes an extra argument (2). - This is the cursor used to store validation errors. We're using
data
to bind the form fields to andui-state
to store validation results in. There's no technical reason we cannot usedata
for this but separating this makes it cleaner. - Again, we use the helpers from
reforms.validation
. - Here we call our function which will perform validation
Here's the sign up function. It shows an alert if data validates:
(defn sign-up!
[data ui-state]
(when (v/validate! ;; 1
data ;; 2
ui-state ;; 3
(v/present [:login] "Enter login name") ;; 4
(v/equal [:password1] [:password2] "Passwords do not match")
(v/present [:password1] "Choose password")
(v/present [:password2] "Re-enter password"))
(js/alert "Signed up!"))
validate!
returns a truthy value if data is valid.- This is data to validate.
- Cursor to store validation results.
- Validators.
Here's what happens after you click "Sign up" while all fields are empty:
To satisfy your curiosity, here are the contents of ui-state
:
{:validation-errors [{:korks #{[:login]}, :error-message "Enter login name"}
{:korks #{[:password1]}, :error-message "Choose password"}
{:korks #{[:password2]}, :error-message "Re-enter password"}]}
A slightly richer example: Om (demo) Reagent (demo).
For the list of available validators, see the API Reference.
Custom validators
A validator is a function that returns a lambda that takes some data and returns nil
or a validation error. Let's create a custom validation that checks if data is a positive number:
(defn positive-number?
[s]
(pos? (js/parseInt s)))
(defn positive-number
[korks error-message] ;; 1
(fn [cursor] ;; 2
(when-not (positive-number? (get-in cursor korks)) ;; 3
(v/validation-error [korks] error-message)))) ;; 4
- The arguments here are up to you. In this example we pass
korks
pointing to data we want to validate and the error message. This is a typical pattern. - The actual function our validator returns takes
cursor
. - Check if it's a positive number.
- Build and return an error if it's not.
While we're at it, we could make it more readable with the built-in is-true
validator:
(defn positive-number
[korks error-message]
(v/is-true korks positive-number? error-message))
Either way, you can use your brand new validator like a pro:
(validate!
data
ui-state
(positive-number [:age] "Age must be a positive number"))
Forcing errors
Validation errors may be forced which comes useful when using external APIs etc. Observe:
(v/validate!
customer
ui-state
(v/force-error [:server-error] "An error has occurred"))
You'd normally call it from an asynchronous error handler, go block etc.
You can either have a form field show the error if it makes sense by passing its korks to force-error
or use the error-alert
helper to render the error:
(v/error-alert [:server-error])
Note that error-alert
can render any number of custom errors like so:
(v/error-alert [:auth-error] [:twitter-error])
Tables
Starting with version 0.4.0 Reforms support HTML tables with optional row selection fully stylable using CSS and compatible with Bootstrap table classes (if you use Bootstrap in the first place).
For live example see this demo (source).
Simple table
This is how you create a simple table, just provide a vector with map per each row:
(t/table [{:name "Tom"} {:name "Jerry"} {:name "Mickey"} {:name "Minnie"}])
Here we create just one column.
Column names
It's usually a good idea to give columns human-friendly titles:
(t/table [{:name "Tom"} {:name "Jerry"} {:name "Mickey"} {:name "Minnie"}]
:columns {:name "Hero name"})
Attributes
As with all controls, you can specify optional attributes; they will be applied to the
element (see https://github.com/r0man/sablono#html-attributes):(t/table {:key "hero-table" ;; Unique React key to avoid warnings.
:class "table-striped"} ;; Bootstrap table style, see http://getbootstrap.com/css/#tables
[{:name "Tom"} {:name "Jerry"} {:name "Mickey"} {:name "Minnie"}]
:columns {:name "Hero name"})
Row selection
As an option, you can enable row selection using checkboxes. Current selection is stored in an atom/cursor (as a set of unique row ids). These row ids are provided through a user-defined function, here we use a separate :id column (which isn't visible to the user):
(t/table {:key "rs-table"}
[{:name "Tom" :id 1} {:name "Jerry" :id 2} {:name "Mickey" :id 3} {:name "Minnie" :id 4}]
:columns {:name "Hero name"}
:checkboxes {:selection data
:path [:selected]
:row-id :id})
Each selected row gets class "table-row-selected" which you can use for styling.
See the API Reference.
Assorted topics
Hiding labels
Starting with version 0.4.0 labels are optional; for example the text box below will be displayed without a label:
(f/text data [:name])
Element attributes
Each form helper accepts React attributes as the first argument. These attributes will be handed over to React (see https://github.com/r0man/sablono#html-attributes)
(text {:key "name-1"} "Name" user [:name])
Attributes are optional, this form will work as well.
(text "Name" user [:name])
Placeholders for empty text boxes
You can add a placeholder shown when the text box is empty using a :placeholder
option:
(f/text "Your name" data [:name] :placeholder "Enter your name here")
It also works for textarea
and other controls based on html5-input
such as password
, datetime-local
, email
and others.
Using radio buttons
When using radio buttons remember to provide a value, for instance:
(f/form
(f/radio "Data" app-state [:current-view] :data)
(f/radio "Groups" app-state [:current-view] :groups))
Showing warnings
In addition to validation proper, text
, password
and other controls based on html5-input
support warnings:
(text "City" [:city] :warn-fn #(when-not (= "Kansas" %) "We're not in Kansas anymore")
Note that by default a Font Awesome icon is used to show the warning icon. You can override this using (set-options! [:icon-warning] "...")
.
Configuration options
You can configure global options using set-options!
. See this for details.
Here's a quick example:
;; Set background of every form to red color.
(set-options! {:form {:attrs {:style {:background-color "red"}}}})
Demos
Om
- Hello world source demo
- Dynamic form with customizations source demo
- Available controls source demo
- Validation source demo
- Background operations source demo
Reagent
- Hello world source demo
- Dynamic form with customizations source demo
- Available controls source demo
- Validation source demo
- Background operations source demo
Rum
- Hello world source demo
- Dynamic form with customizations source demo
- Available controls source demo
- Validation source demo
- Background operations source demo
FAQ
How do I submit the form when the user presses ENTER?
Use the :on-submit
attribute and pass the same function you use to handle clicks:
(form
{:on-submit #(do-something customer)}
(text "First name" "Enter first name" customer [:first])
...
(f/form-buttons
(f/button-primary "Save" #(do-something customer))))
Note: If :on-submit
is set, the resulting form will include a hidden submit button.
How to affect changes when user clicks a button?
Because form helpers bind to data, everything user types in is automatically synchronized. If this isn't what you need, create a copy of data before handing it over to the form and then copy it back on save.
How to show an operation is in progress?
Buttons and most form helpers accept an :in-progress
option you can use like this:
(button "Start" #(...) :in-progress true)
In addition, in case of buttons it's usually a good idea to disable them:
(button "Start" #(...) :in-progress true :disabled true)
See this example: Om (demo) Reagent (demo) Rum (demo)
I'm getting Each child in an array should have a unique "key" prop. Why?
If you use Om, it's likely the warning is sabl0no-related (see this).
In your own code avoid passing child elements as a sequence whenever possible:
[:ul
(for [item items]
[:li item])]
with:
(into
[:ul]
(for [item items]
[:li item]))
If you need to pass a sequence, use attributes to set React key. For example, use code similar to this:
(let [items [{:title "foo" :id 1} {:title "bar" :id 2}]]
[:ul
(for [{:keys [title id]} items]
[:li {:key id} title])])
On the other hand, if you do find a bug in Reforms, please do report it here.
Can I bind to local component state (Om-specific)?
Yes, there's experimental support for this, just remember to use render-state
instead of render
:
(defn simple-view
[_ owner]
(reify
om/IRenderState
(render-state [_ _]
(f/text "Your name" owner [:name] :placeholder "Type your name here"))))
You can also store validation data in local state which may be useful even if you store the actual data in an atom.
A slightly more complete example: source demo
** This is an experimental feature. Please report any bugs. **
API Reference
Please feel free to tweet me @martinbilski or drop me an email: gyamtso at gmail dot com.
TBD
- Keep Readme short, move most of it to wiki.
- Contact library authors.
- Add tabs. Update 'controls' example. Blog post.
- Port tests.
Credits
Aspasia Beneti is the author and maintainer of Rum bindings for Reforms.
License
Copyright Β© 2015 Designed.ly, Marcin Bilski
The use and distribution terms for this software are covered by the Eclipse Public License which can be found in the file LICENSE at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.