Skip to content

bilus/reforms

Repository files navigation

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

Add om-reforms to :dependencies in project.clj:

Clojars Project

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:

Clojars Project

(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:

Clojars Project

(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)))))

Hello world

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))))))

Horizontal orientation

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))))))

Form wrapped in a panel

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]))

Horizontal orientation

Click!

Vertical orientation

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
  1. We use reforms.validation/form. Note that it takes an extra argument (2).
  2. This is the cursor used to store validation errors. We're using data to bind the form fields to and ui-state to store validation results in. There's no technical reason we cannot use data for this but separating this makes it cleaner.
  3. Again, we use the helpers from reforms.validation.
  4. 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!"))
  1. validate! returns a truthy value if data is valid.
  2. This is data to validate.
  3. Cursor to store validation results.
  4. 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
  1. 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.
  2. The actual function our validator returns takes cursor.
  3. Check if it's a positive number.
  4. 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

Reagent

Rum

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. **

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.

About

Beautiful Bootstrap 3 forms for Om, Reagent and Rum.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •