├── .projectile ├── doc └── images │ ├── text.png │ ├── sample.png │ ├── warning.png │ ├── progress.png │ ├── hello-world.png │ ├── validation-1.png │ ├── hello-world-panel.png │ ├── hello-world-horizontal.png │ ├── hello-world-buttons-hor.png │ └── hello-world-buttons-ver.png ├── test └── reforms │ ├── runner.cljs │ └── core │ ├── react_keys_test.cljs │ └── impl_test.cljs ├── .gitignore ├── repl.clj ├── src └── reforms │ ├── binding │ ├── protocol.cljs │ └── core.cljs │ ├── core │ ├── import.clj │ ├── react_keys.cljs │ ├── options.cljs │ └── impl.cljs │ ├── core.clj │ ├── validation.clj │ ├── table.cljs │ ├── validation.cljs │ └── core.cljs ├── deploy-docs.sh ├── deploy.sh ├── project.clj ├── reforms.iml ├── LICENSE └── README.md /.projectile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/images/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/text.png -------------------------------------------------------------------------------- /doc/images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/sample.png -------------------------------------------------------------------------------- /doc/images/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/warning.png -------------------------------------------------------------------------------- /doc/images/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/progress.png -------------------------------------------------------------------------------- /doc/images/hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/hello-world.png -------------------------------------------------------------------------------- /doc/images/validation-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/validation-1.png -------------------------------------------------------------------------------- /doc/images/hello-world-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/hello-world-panel.png -------------------------------------------------------------------------------- /doc/images/hello-world-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/hello-world-horizontal.png -------------------------------------------------------------------------------- /doc/images/hello-world-buttons-hor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/hello-world-buttons-hor.png -------------------------------------------------------------------------------- /doc/images/hello-world-buttons-ver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bilus/reforms/HEAD/doc/images/hello-world-buttons-ver.png -------------------------------------------------------------------------------- /test/reforms/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [reforms.core.react-keys-test] 4 | [reforms.core.impl-test])) 5 | 6 | (doo-tests 'reforms.core.react-keys-test 7 | 'reforms.core.impl-test) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | out/ 14 | .DS_Store 15 | /doc/css 16 | /doc/*.html 17 | /doc/*.md 18 | /doc/js 19 | /resources/public/js/testable.js 20 | examples/ 21 | /.cljs_rhino_repl 22 | /.cljs_rhino_repl.bak 23 | 24 | -------------------------------------------------------------------------------- /repl.clj: -------------------------------------------------------------------------------- 1 | (require 'cljs.repl) 2 | (require 'cljs.build.api) 3 | ;(require 'cljs.repl.browser) 4 | (require 'cljs.repl.node) 5 | 6 | (cljs.build.api/build "src" 7 | {:main 'reforms.core 8 | :output-to "out/main.js" 9 | :verbose true}) 10 | 11 | ;(cljs.repl/repl (cljs.repl.browser/repl-env) 12 | (cljs.repl/repl (cljs.repl.node/repl-env) 13 | :watch "src" 14 | :output-dir "out") 15 | 16 | -------------------------------------------------------------------------------- /src/reforms/binding/protocol.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.binding.protocol 2 | (:refer-clojure :exclude [-deref -reset! -swap!])) 3 | 4 | ;; "No protocol method IBinding. ..." error around here probably means you forgot 5 | ;; to require `reforms.binding.*` in a namespace (where * corresponds to the name of the 6 | ;; react wrapper library you are using) e.g. `reforms.binding.om` or `reforms.binding.reagent`. 7 | (defprotocol IBinding 8 | (-valid? [x]) 9 | (-deref [x]) 10 | (-reset! [x v] [x ks v]) 11 | (-swap! [x f] [x ks f]) 12 | (-get-in [x ks]) 13 | (-path [x])) 14 | -------------------------------------------------------------------------------- /src/reforms/core/import.clj: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.core.import 8 | (:require [cljs.analyzer.api :as ana-api])) 9 | 10 | (defmacro import-vars [[_quote ns]] 11 | `(do 12 | ~@(->> 13 | (ana-api/ns-interns ns) 14 | (remove (comp :macro second)) 15 | (map (fn [[k# _]] 16 | `(def ~(symbol k#) ~(symbol (name ns) (name k#)))))))) 17 | -------------------------------------------------------------------------------- /deploy-docs.sh: -------------------------------------------------------------------------------- 1 | # Build Home page and the reference + examples. 2 | cd ../reforms && 3 | BRANCH="$(git rev-parse --abbrev-ref HEAD)" && 4 | echo "Generate docs" && 5 | lein doc && 6 | mkdir -p /tmp/reforms/doc && 7 | cp -r doc/ /tmp/reforms/doc/ && 8 | echo "Checkout and pull gh-pages" && 9 | git checkout gh-pages && 10 | git pull origin gh-pages 11 | 12 | cp -r /tmp/reforms/doc . && 13 | echo "Copy examples" && 14 | mkdir -p examples/om && 15 | cp -r ../om-reforms/examples/ examples/om && 16 | mkdir -p examples/reagent && 17 | cp -r ../reagent-reforms/examples/ examples/reagent && 18 | mkdir -p examples/rum && 19 | cp -r ../rum-reforms/examples/ examples/rum && 20 | sleep 5 && # Locked git 21 | echo "Push to git" && 22 | git add doc/ examples/ && 23 | git commit -m "Update Github Pages." && 24 | git push origin gh-pages && 25 | echo "Switch back to $BRANCH branch" 26 | 27 | git checkout $BRANCH 28 | -------------------------------------------------------------------------------- /src/reforms/core/react_keys.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.core.react-keys 2 | (:require [reforms.binding.core :as binding] 3 | [clojure.string :as str]) 4 | (:import [goog.ui IdGenerator])) 5 | 6 | (defn to-str 7 | [x] 8 | (cond 9 | (binding/valid? x) (->> (binding/path x) 10 | (map to-str) 11 | (str/join "-")) 12 | (keyword? x) (name x) 13 | (vector? x) (->> x 14 | (map to-str) 15 | (str/join "-")) 16 | :else (str x))) 17 | 18 | (defn gen-key [& args] 19 | (or (not-empty (str/replace (->> args 20 | (remove nil?) 21 | (map to-str) 22 | (remove empty?) 23 | (str/join "-") 24 | (str/lower-case)) #"[^a-zA-Z0-9_$]" "-")) 25 | (.getNextUniqueId (.getInstance IdGenerator)))) -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | read -p "Are version numbers in sync? " -n 1 -r 2 | echo # (optional) move to a new line 3 | if [[ $REPLY =~ ^[Yy]$ ]] 4 | then 5 | # Build Reforms core. 6 | git checkout master && 7 | doctoc README.md && # npm install -g doctoc 8 | git add README.md && 9 | git commit -m "Regenerate TOC in Readme." && 10 | git push origin master 11 | lein deploy clojars && 12 | 13 | # Build om-reforms examples. 14 | cd ../om-reforms && 15 | git checkout master && 16 | lein deps && lein clean && lein cljsbuild once && 17 | lein deploy clojars && 18 | 19 | # Build reagent-reforms examples. 20 | cd ../reagent-reforms && 21 | git checkout master && 22 | lein deps && lein clean && lein cljsbuild once && 23 | lein deploy clojars && 24 | 25 | # Build rum-reforms examples. 26 | cd ../rum-reforms && 27 | git checkout master && 28 | lein deps && lein clean && lein cljsbuild once && 29 | lein deploy clojars && 30 | 31 | ./deploy-docs.sh 32 | fi 33 | 34 | -------------------------------------------------------------------------------- /src/reforms/core.clj: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.core) 8 | 9 | (defmacro with-options 10 | "Specify options for a block. 11 | 12 | ** Important:** Use only inside a component and never outside om.core/build or om.core/root because it uses 13 | dynamic binding. To specify global options use [set-options!]. 14 | 15 | Example: 16 | 17 | (with-options {:form {:horizontal true}} 18 | (form 19 | (text ...) 20 | (button ...))" 21 | [options & body] 22 | `(binding [reforms.core.options/*options* (reforms.core.options/merge-options ~options)] 23 | ~@body)) 24 | 25 | -------------------------------------------------------------------------------- /src/reforms/binding/core.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.binding.core 8 | (:refer-clojure :exclude [reset! swap! deref get-in -deref -reset!]) 9 | (:require 10 | [reforms.binding.protocol :refer [-valid? -deref -reset! -get-in -path -swap!]])) 11 | 12 | (defn valid? [x] 13 | (-valid? x)) 14 | 15 | (defn deref [x] 16 | (-deref x)) 17 | 18 | (defn reset! 19 | ([x v] 20 | (-reset! x v)) 21 | ([x ks v] 22 | (if (not-empty ks) 23 | (-reset! x ks v) 24 | (-reset! x v)))) 25 | 26 | (defn swap! 27 | ([x f] 28 | (-swap! x 7)) 29 | ([x ks f] 30 | (if (not-empty ks) 31 | (-swap! x ks f) 32 | (-swap! x f)))) 33 | 34 | (defn get-in [x ks] 35 | (if (not-empty ks) 36 | (-get-in x ks) 37 | (-deref x))) 38 | 39 | (defn path [x] 40 | (-path x)) 41 | -------------------------------------------------------------------------------- /test/reforms/core/react_keys_test.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.core.react-keys-test 2 | (:require [reforms.core.react-keys :refer [gen-key]] 3 | [reforms.binding.protocol :refer [IBinding]] 4 | [cljs.test :refer-macros [deftest is testing run-tests]])) 5 | 6 | (defn make-cursor [path] 7 | (reify 8 | IBinding 9 | (-valid? [x] 10 | true) 11 | (-path [x] 12 | path))) 13 | 14 | (extend-type default 15 | IBinding 16 | (-valid? [x] 17 | false) 18 | (-path [x])) 19 | 20 | (deftest gen-key-test 21 | (testing "given no args returns unique id" 22 | (is (not= (gen-key) (gen-key)))) 23 | (testing "given nil args returns unique id" 24 | (is (not= (gen-key nil nil) (gen-key nil nil)))) 25 | (testing "given strings joins them with hyphens" 26 | (is (= "label-this-is-something") (gen-key "label" "this-is-something"))) 27 | (testing "converts strings into lowercase identifiers" 28 | (is (= "long-label-this--is-something") (gen-key "Long label" "This,?is-something"))) 29 | (testing "cursor, path and symbol" 30 | (is (= "path1-path2" (gen-key [:path1 :path2]))) 31 | (is (= "path1-path2" (gen-key :path1 :path2))) 32 | (is (= "foo-cursor-path-bar-path1-path2" (gen-key "foo" 33 | (make-cursor [:cursor :path]) 34 | :bar 35 | [:path1 :path2]))))) -------------------------------------------------------------------------------- /test/reforms/core/impl_test.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.core.impl-test 2 | (:require [reforms.core.impl :refer [parse-args gen-dom-id]] 3 | [cljs.test :refer-macros [deftest is are testing run-tests]])) 4 | 5 | 6 | (deftest parse-args-test 7 | (are [result args opts] (= result (parse-args args opts)) 8 | [{} 5 6] [5 6] [[map? {}]] 9 | [{:foo "bar"} "" 5 6] [{:foo "bar"} 5 6] [[map? {}] [string? ""]] 10 | [{} "" 5 6] [5 6] [[map? {}] [string? ""]] 11 | [{} 5 6] [5 6] [[map? {}] [number? nil]] 12 | [{} nil 5 6] [5 6] [[map? {}] [(complement number?) nil]] 13 | [{:foo "bar"} nil 5 6] [{:foo "bar"} 5 6] [[map? {}] [(complement number?) nil]] 14 | [{} "a" 5 6] ["a" 5 6] [[map? {}] [(complement number?) nil]] 15 | [{:foo "bar"} "a" 5 6] [{:foo "bar"} "a" 5 6] [[map? {}] [(complement number?) nil]] 16 | [{:foo "bar"} nil nil 5 6] [{:foo "bar"} 5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]] 17 | [{} nil nil 5 6] [5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]] 18 | [{} "a" nil 5 6] ["a" 5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]] 19 | [{:foo "bar"} "a" nil 5 6] [{:foo "bar"} "a" 5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]] 20 | [{:foo "bar"} "a" "b" 5 6] [{:foo "bar"} "a" "b" 5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]] 21 | [{} "a" "b" 5 6] ["a" "b" 5 6] [[map? {}] [(complement number?) nil] [(complement number?) nil]])) 22 | 23 | (deftest gen-dom-id-test 24 | (are [dom-id korks] (= dom-id (gen-dom-id korks)) 25 | "a" [:a] 26 | "a-b" [:a :b] 27 | "a-b" ["a" "b"] 28 | "1-2" [1 2])) 29 | 30 | -------------------------------------------------------------------------------- /src/reforms/validation.clj: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.validation) 8 | 9 | (defmacro validating-fields 10 | "Low-level macro to wrap validating fields. Normally you'll use the [[reforms.validation/form]] macro. 11 | 12 | Example: 13 | 14 | (let [errors (validation-errors ui-state)] 15 | (form 16 | (validating-fields 17 | errors 18 | (text ...) 19 | (text ...))))" 20 | [validation-errors & body] 21 | `(reforms.validation/validating-fields-fn ~validation-errors (fn [] 22 | (list ~@body)))) 23 | 24 | (defn parse-args ;; TODO: Duplicated in reforms.core; move to .cljc 25 | [args] 26 | (if (map? (first args)) 27 | [(first args) (rest args)] 28 | [{} args])) 29 | 30 | (defmacro form 31 | "Wrapper for [[reforms.core/form]]. **This is a macro.** 32 | 33 | It accepts the same args as [[reforms.core/form]] except there's an extra one in front: `cursor` to store validation results in." 34 | [cursor & args] 35 | (let [[attrs [& elems]] (parse-args args)] 36 | `(do 37 | (assert (reforms.binding.core/valid? ~cursor) "The first argument to reforms.validation/form before optional attributes must be a valid binding target.") 38 | (apply reforms.core/form 39 | ~attrs 40 | (reforms.validation/validating-fields 41 | (reforms.validation/validation-errors ~cursor) 42 | ~@elems))))) 43 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject reforms "0.4.4-SNAPSHOT" 2 | :description "Form helpers for React-based Clojurescript libraries." 3 | :url "http://bilus.github.io/reforms/" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0" :scope "provided"] 7 | [org.clojure/clojurescript "1.7.122" :scope "provided"]] 8 | :jar-exclusions [#"\.cljx|\.swp|\.swo|\.DS_Store"] 9 | :auto-clean false 10 | :source-paths ["src"] 11 | 12 | :plugins [[lein-cljsbuild "1.1.0"] 13 | [lein-doo "0.1.4"]] 14 | 15 | :clean-targets ^{:protect false} ["target/" 16 | "resources/public/js/testable.js"] 17 | 18 | :codox {:language :clojurescript 19 | :include [reforms.core reforms.core.options reforms.validation reforms.table] 20 | :src-dir-uri "http://github.com/bilus/reform/blob/master/" 21 | :src-linenum-anchor-prefix "L" 22 | :defaults {:doc/format :markdown}} 23 | 24 | :profiles {:doc {:dependencies [[org.clojure/clojurescript "0.0-2985"]]} 25 | :dev {:dependencies [[org.clojure/core.async "0.1.346.0-17112a-alpha"] 26 | [com.cemerick/piggieback "0.2.1"] 27 | ] 28 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}}} 29 | 30 | :aliases {"doc" ["with-profile" "doc" "doc"] 31 | "cljsbuild" ["with-profile" "dev" "cljsbuild"]} 32 | 33 | :cljsbuild {:builds {:test {:source-paths ["src" "test"] 34 | :compiler {:output-to "resources/public/js/testable.js" 35 | :main 'reforms.runner 36 | :optimizations :whitespace}}}}) 37 | -------------------------------------------------------------------------------- /src/reforms/core/options.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.core.options 8 | "Configuration options. You can override the defaults using [[set-options!]] or [[with-options]].") 9 | 10 | (def ^:dynamic *options* 11 | "Note that top-level key corresponds to a helper in [[reforms.core]]. 12 | 13 | Common for all form helpers: 14 | 15 | - :attrs - specify attributes to be passed to React node; see see https://github.com/r0man/sablono#html-attributes 16 | 17 | Example: 18 | 19 | ;; Set background of every form to red color. 20 | (set-options! {:form {:attrs {:style {:background-color \"red\"}}}}) 21 | 22 | - :icon-warning - icon to represent a warning 23 | - :form - options for [[reforms.core/form]] 24 | - :horizontal - set to true for horizontal orientation; see http://getbootstrap.com/css/#forms-horizontal 25 | - :label-column-class - class for the label column (horizontal orientation only), e.g. \"col-sm-3\" 26 | - :input-column-class - class for the input column (horizontal orientation only), e.g. \"col-sm-9\" 27 | - :panel - options for [[reforms.core/panel]] 28 | - :icon-close - close panel icon 29 | - :html5-input - options for [[reforms.core/html5-input]], common for all helpers based on it, e.g. [[reforms.core/text]] 30 | - :text - options for [[reforms.core/text]] 31 | Same as for :html-5-input 32 | - group-title - options for [[reforms.core/group-title]] 33 | - :tag - tag to use, e.g. :h4 34 | - button-group - options for [[reforms.core/button-group]] 35 | - :spinner - options for [[reforms.core/spinner]]. 36 | 37 | ... Each helper is supported - :password, :select, :color etc. see [[reforms.core]] for available form helpers... 38 | 39 | 40 | For the defaults, see the source." 41 | {:form {:horizontal false 42 | :label-column-class "col-sm-3" 43 | :input-column-class "col-sm-7"} 44 | :panel {:icon-close "fa fa-times"} 45 | :html5-input {:icon-warning "fa fa-warning"} 46 | :group-title {:tag :h2} 47 | :button-group {:align "text-left"} 48 | :spinner {:attrs {:class "fa fa-clock-o fa-spin"}} 49 | :error-label {:attrs {:class "error"}} 50 | :warning-label {:attrs {:class "warning"}}}) 51 | 52 | (defn merge-options 53 | ([options] 54 | (merge-options *options* options)) 55 | ([prev-options new-options] 56 | (merge-with 57 | (fn [old new] 58 | (cond 59 | (map? old) (merge-options old new) 60 | :else new)) 61 | prev-options 62 | new-options))) 63 | 64 | (defn get-options 65 | "See [[reforms.core/get-options]]" 66 | [ks] 67 | (get-in *options* ks)) 68 | 69 | (defn set-options! 70 | "See [[reforms.core/set-options!]]" 71 | [options] 72 | (set! *options* (merge-options options))) -------------------------------------------------------------------------------- /reforms.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/reforms/table.cljs: -------------------------------------------------------------------------------- 1 | (ns reforms.table 2 | (:require [reforms.core.impl :as impl] 3 | [reforms.binding.core :as binding] 4 | [clojure.set :as set])) 5 | 6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 7 | ;; Implementation 8 | 9 | (defn simple-checkbox 10 | [checked & {:keys [on-click]}] 11 | [:input {:onChange #(on-click (.. % -target -checked)) 12 | :checked checked 13 | :type "checkbox"}]) 14 | 15 | (defn all-selected? 16 | [selected-values all-values] 17 | (set/subset? (into #{} all-values) selected-values)) 18 | 19 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 20 | ;; Public 21 | 22 | (defn table 23 | "Renders a table of `rows` (col => value). An optional map of columns to labels can be specified which also 24 | restricts which columns are visible. 25 | 26 | Arguments: 27 | 28 | [attrs] rows [options] 29 | 30 | - attrs - (optional) attributes for handed over to React (see https://github.com/r0man/sablono#html-attributes) 31 | - rows - a vector of col->val maps 32 | - options - (optional) options to customize the table 33 | 34 | Options: 35 | 36 | - :columns - map of keys used in `rows` to human-friendly column names 37 | - :checkboxes - use this to support row selection using checkboxes, the map contains: 38 | :selection - atom/cursor to keep selected rows in; either nil or a set 39 | :path - (optional) korks/path into the selection atom/cursor 40 | :row-id - function applied to row map to calculate a unique row id to put into the selection set 41 | :nil-selects-all? - (optional) selection set to nil (as opposed to #{}) selects all rows (default: false) 42 | See the second example. 43 | 44 | Example: 45 | 46 | (table [{:name \"Tom\" :id 12} {:name \"Jerry\" :id 23}] 47 | :columns {:name \"Hero name\"}) 48 | 49 | Result: 50 | 51 | | Hero name | 52 | | Tom | 53 | | Jerry | 54 | 55 | As an option, each row can have an on/off checkbox to the left; to turn this on, `checkboxes` must be a map 56 | with :cursor and :korks pointing to where to store the selection. The resulting selection 57 | is either a set containing values or nil; the latter means that all rows are selected. 58 | The values are taken from the first val in the respective row or a result of :value function of 59 | signature (fn [row]). 60 | 61 | Example: 62 | 63 | (table [{:name \"Tom\" :id 12} {:name \"Jerry\" :id 23}] 64 | :checkboxes {:selection app-state 65 | :path [:selected-heroes] 66 | :row-id :id} 67 | :columns {:name \"Hero name\"}) 68 | 69 | | [ ] Hero name | 70 | | [x] Tom | 71 | | [ ] Jerry | 72 | 73 | ;; For the above selection: 74 | (get-in app-state [:selected-heroes]) => #{12} 75 | 76 | Depending on the initial value of `app-state` in the above example the table start either 77 | 78 | a) with all checkboxes on for `app-state` containing {:selected-heroes nil}, or 79 | b) with no selection with {:selected-heroes #{}}." 80 | [& args] 81 | (let [[attrs [rows & {:keys [columns checkboxes xf] :or {columns {} xf (fn [col val] val)}}]] (impl/resolve-args [:table] {:class "table"} args) 82 | labels (into {} columns) 83 | col-keys (or (not-empty (map first columns)) 84 | (->> rows 85 | (mapcat keys) 86 | distinct)) 87 | {selection :selection selection-path :path row-id-fn :row-id nil-selects-all? :nil-selects-all? :or {row-id-fn (comp first vals)}} checkboxes 88 | selected (when selection (or (binding/get-in selection selection-path) #{})) ; selection-path may be nil 89 | select-all (when selection (and nil-selects-all? (nil? (binding/get-in selection selection-path))))] 90 | (into 91 | [:table attrs] 92 | [(into 93 | [:thead] 94 | [(into 95 | [:tr] 96 | (concat 97 | (when checkboxes 98 | (let [all-values (map row-id-fn rows)] 99 | (list 100 | (let [] 101 | [:th (simple-checkbox (or select-all (all-selected? selected all-values)) 102 | :on-click (fn [checked] 103 | (binding/swap! selection selection-path 104 | (fn [prev-selected] 105 | (if checked 106 | (into (or prev-selected #{}) all-values) 107 | #{})))))])))) 108 | (for [col col-keys] 109 | [:th (or (labels col) col)])))]) 110 | (into 111 | [:tbody] 112 | (for [row rows] 113 | (let [value (row-id-fn row) 114 | row-selected (and checkboxes 115 | (or select-all (selected (row-id-fn row))))] 116 | (into 117 | [:tr (when row-selected 118 | {:class "table-row-selected"})] 119 | (concat 120 | (when checkboxes 121 | [[:td (simple-checkbox row-selected 122 | :on-click (fn [checked] 123 | (binding/swap! selection selection-path 124 | (fn [prev-selected] 125 | (let [all-values (map row-id-fn rows)] 126 | (cond 127 | select-all (disj (into #{} all-values) value) 128 | checked (conj (or prev-selected #{}) value) 129 | :else (disj (or prev-selected #{}) value)))))))]]) 130 | (for [col col-keys] 131 | [:td (xf col (row col))]))))))]))) 132 | -------------------------------------------------------------------------------- /src/reforms/core/impl.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) 2015 Designed.ly, Marcin Bilski 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 4 | ; By using this software in any fashion, you are agreeing to be bound by the terms of this license. 5 | ; You must not remove this notice, or any other, from this software. 6 | 7 | (ns reforms.core.impl 8 | (:require [reforms.binding.core :as binding] 9 | [reforms.core.options :refer [get-options]] 10 | [reforms.core.react-keys :refer [gen-key]] 11 | [clojure.string :as str]) 12 | (:refer-clojure :exclude [time]) 13 | (:import [goog.ui IdGenerator])) 14 | 15 | (declare spinner feedback-icon) 16 | 17 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 18 | ;;; Argument parsing 19 | 20 | (defn extend-attrs 21 | [attrs extensions] 22 | (merge-with (fn [& vals] 23 | (let [result (let [vals' (remove nil? vals)] 24 | (if (some #(satisfies? Fn %) vals') 25 | (fn [& args] (last (map (fn [f] (apply f args)) vals'))) 26 | (str/join " " vals')))] 27 | result)) 28 | attrs 29 | extensions)) 30 | 31 | (defn override-attrs 32 | [old new] 33 | (merge-with (fn [old new] 34 | (cond 35 | (map? old) (override-attrs old new) 36 | :else new)) 37 | old 38 | new)) 39 | 40 | 41 | (defn merge-attrs 42 | [defaults overrides extensions] 43 | (as-> overrides $ 44 | (override-attrs defaults $) 45 | (extend-attrs $ extensions))) 46 | 47 | (defn parse-args 48 | ([args opt-args result] 49 | (let [[opt-pred def] (first opt-args) 50 | arg (first args)] 51 | (cond 52 | (nil? arg) result 53 | (nil? opt-pred) (recur (rest args) opt-args (conj result arg)) 54 | (opt-pred arg) (recur (rest args) (rest opt-args) (conj result arg)) 55 | :else (recur args (rest opt-args) (conj result def))))) 56 | ([args opt-args] 57 | (parse-args args opt-args [])) 58 | ([args] 59 | (parse-args args [] []))) 60 | 61 | (defn resolve-args 62 | ([ks ext-attrs args & [opt-args]] 63 | (let [[attrs & rest-args] (parse-args args (or opt-args [[map? {}]]))] 64 | [(merge-attrs 65 | (->> ks 66 | (map #(get-options [% :attrs])) 67 | (reduce (fn [attrs crn-attrs] 68 | (merge-attrs attrs crn-attrs {})))) 69 | attrs 70 | ext-attrs) 71 | rest-args]))) 72 | 73 | (defn parse-options 74 | [args] 75 | (let [[options rest-args] (split-with (comp keyword? first) (partition-all 2 args))] 76 | [(apply hash-map (mapcat identity options)) (mapcat identity (or rest-args []))])) 77 | 78 | (defn attrs? 79 | [arg] 80 | (and 81 | (not (binding/valid? arg)) 82 | (map? arg))) 83 | 84 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 85 | ;; Generic helpers 86 | 87 | (defn unstr-option 88 | "Converts an option converted to string to be used in
element (see https://github.com/r0man/sablono#html-attributes): 459 | 460 | ```clojure 461 | (t/table {:key "hero-table" ;; Unique React key to avoid warnings. 462 | :class "table-striped"} ;; Bootstrap table style, see http://getbootstrap.com/css/#tables 463 | [{:name "Tom"} {:name "Jerry"} {:name "Mickey"} {:name "Minnie"}] 464 | :columns {:name "Hero name"}) 465 | ``` 466 | 467 | 468 | #### Row selection 469 | 470 | 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). 471 | These row ids are provided through a user-defined function, here we use a separate :id column (which isn't visible to the user): 472 | 473 | ```clojure 474 | (t/table {:key "rs-table"} 475 | [{:name "Tom" :id 1} {:name "Jerry" :id 2} {:name "Mickey" :id 3} {:name "Minnie" :id 4}] 476 | :columns {:name "Hero name"} 477 | :checkboxes {:selection data 478 | :path [:selected] 479 | :row-id :id}) 480 | ``` 481 | 482 | Each selected row gets class "table-row-selected" which you can use for styling. 483 | 484 | See the [API Reference](http://bilus.github.io/reforms/doc/reforms.table.html#var-table). 485 | 486 | ### Assorted topics 487 | 488 | #### Hiding labels 489 | 490 | Starting with version 0.4.0 labels are optional; for example the text box below will be displayed without a label: 491 | 492 | ```clojure 493 | (f/text data [:name]) 494 | ``` 495 | 496 | #### Element attributes 497 | 498 | 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) 499 | 500 | ```clojure 501 | (text {:key "name-1"} "Name" user [:name]) 502 | ``` 503 | 504 | Attributes are optional, this form will work as well. 505 | 506 | ```clojure 507 | (text "Name" user [:name]) 508 | ``` 509 | 510 | #### Placeholders for empty text boxes 511 | 512 | You can add a placeholder shown when the text box is empty using a `:placeholder` option: 513 | 514 | ```clojure 515 | (f/text "Your name" data [:name] :placeholder "Enter your name here") 516 | ``` 517 | 518 | It also works for `textarea` and other controls based on `html5-input` such as `password`, `datetime-local`, `email` and others. 519 | 520 | #### Using radio buttons 521 | 522 | When using radio buttons remember to provide a value, for instance: 523 | 524 | ```clojure 525 | (f/form 526 | (f/radio "Data" app-state [:current-view] :data) 527 | (f/radio "Groups" app-state [:current-view] :groups)) 528 | ``` 529 | 530 | #### Showing warnings 531 | 532 | In addition to validation proper, `text`, `password` and other controls based on `html5-input` support warnings: 533 | 534 | ```clojure 535 | (text "City" [:city] :warn-fn #(when-not (= "Kansas" %) "We're not in Kansas anymore") 536 | ``` 537 | 538 | 539 | 540 | Note that by default a Font Awesome icon is used to show the warning icon. You can override this using `(set-options! [:icon-warning] "...")`. 541 | 542 | #### Configuration options 543 | 544 | You can configure global options using `set-options!`. See [this](http://bilus.github.io/reforms/doc/reforms.core.options.html) for details. 545 | 546 | Here's a quick example: 547 | 548 | ```clojure 549 | ;; Set background of every form to red color. 550 | (set-options! {:form {:attrs {:style {:background-color "red"}}}}) 551 | ``` 552 | 553 | ### Demos 554 | 555 | #### Om 556 | 557 | - Hello world [source](https://github.com/bilus/om-reforms/tree/master/examples/hello_world) [demo](http://bilus.github.io/reforms/examples/om/hello_world/index.html) 558 | - Dynamic form with customizations [source](https://github.com/bilus/om-reforms/tree/master/examples/simple) [demo](http://bilus.github.io/reforms/examples/om/simple/index.html) 559 | - Available controls [source](https://github.com/bilus/om-reforms/tree/master/examples/controls) [demo](http://bilus.github.io/reforms/examples/om/controls/index.html) 560 | - Validation [source](https://github.com/bilus/om-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/examples/om/validation/index.html) 561 | - Background operations [source](https://github.com/bilus/om-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/om/examples/progress/index.html) 562 | 563 | #### Reagent 564 | 565 | - Hello world [source](https://github.com/bilus/reagent-reforms/tree/master/examples/hello_world) [demo](http://bilus.github.io/reforms/examples/reagent/hello_world/index.html) 566 | - Dynamic form with customizations [source](https://github.com/bilus/reagent-reforms/tree/master/examples/simple) [demo](http://bilus.github.io/reforms/examples/reagent/simple/index.html) 567 | - Available controls [source](https://github.com/bilus/reagent-reforms/tree/master/examples/controls) [demo](http://bilus.github.io/reforms/examples/reagent/controls/index.html) 568 | - Validation [source](https://github.com/bilus/reagent-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/examples/reagent/validation/index.html) 569 | - Background operations [source](https://github.com/bilus/reagent-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/examples/reagent/progress/index.html) 570 | 571 | #### Rum 572 | 573 | - Hello world [source](https://github.com/bilus/rum-reforms/tree/master/examples/hello_world) [demo](http://bilus.github.io/reforms/examples/rum/hello_world/index.html) 574 | - Dynamic form with customizations [source](https://github.com/bilus/rum-reforms/tree/master/examples/simple) [demo](http://bilus.github.io/reforms/examples/rum/simple/index.html) 575 | - Available controls [source](https://github.com/bilus/rum-reforms/tree/master/examples/controls) [demo](http://bilus.github.io/reforms/examples/rum/controls/index.html) 576 | - Validation [source](https://github.com/bilus/rum-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/examples/rum/validation/index.html) 577 | - Background operations [source](https://github.com/bilus/rum-reforms/tree/master/examples/validation) [demo](http://bilus.github.io/reforms/examples/rum/progress/index.html) 578 | 579 | ### FAQ 580 | #### How do I submit the form when the user presses ENTER? 581 | 582 | Use the `:on-submit` attribute and pass the same function you use to handle clicks: 583 | 584 | ```clojure 585 | (form 586 | {:on-submit #(do-something customer)} 587 | (text "First name" "Enter first name" customer [:first]) 588 | ... 589 | (f/form-buttons 590 | (f/button-primary "Save" #(do-something customer)))) 591 | ``` 592 | 593 | **Note:** If `:on-submit` is set, the resulting form will include a hidden submit button. 594 | 595 | #### How to affect changes when user clicks a button? 596 | 597 | 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. 598 | 599 | #### How to show an operation is in progress? 600 | 601 | Buttons and most form helpers accept an `:in-progress` option you can use like this: 602 | 603 | ```clojure 604 | (button "Start" #(...) :in-progress true) 605 | ``` 606 | 607 | In addition, in case of buttons it's usually a good idea to disable them: 608 | 609 | ```clojure 610 | (button "Start" #(...) :in-progress true :disabled true) 611 | ``` 612 | 613 | 614 | 615 | See this example: [Om](https://github.com/bilus/om-reforms/tree/master/examples/progress/) ([demo](http://bilus.github.io/reforms/examples/om/progress/index.html)) 616 | [Reagent](https://github.com/bilus/reagent-reforms/tree/master/examples/progress/) ([demo](http://bilus.github.io/reforms/examples/reagent/progress/index.html)) 617 | [Rum](https://github.com/bilus/rum-reforms/tree/master/examples/progress/) ([demo](http://bilus.github.io/reforms/examples/rum/progress/index.html)) 618 | 619 | #### I'm getting *Each child in an array should have a unique "key" prop*. Why? 620 | 621 | If you use Om, it's likely the warning is sabl0no-related (see [this](https://github.com/r0man/sablono/issues/57)). 622 | 623 | In your own code avoid passing child elements as a sequence whenever possible: 624 | 625 | ```clojure 626 | [:ul 627 | (for [item items] 628 | [:li item])] 629 | ``` 630 | 631 | with: 632 | 633 | ```clojure 634 | (into 635 | [:ul] 636 | (for [item items] 637 | [:li item])) 638 | ``` 639 | 640 | If you need to pass a sequence, use attributes to set React key. For example, use code similar to this: 641 | 642 | ```clojure 643 | (let [items [{:title "foo" :id 1} {:title "bar" :id 2}]] 644 | [:ul 645 | (for [{:keys [title id]} items] 646 | [:li {:key id} title])]) 647 | ``` 648 | 649 | On the other hand, if you do find a bug in Reforms, please do report it [here](https://github.com/bilus/reforms/issues). 650 | 651 | #### Can I bind to local component state (Om-specific)? 652 | 653 | 654 | Yes, there's experimental support for this, just remember to use `render-state` instead of `render`: 655 | 656 | ```clojure 657 | (defn simple-view 658 | [_ owner] 659 | (reify 660 | om/IRenderState 661 | (render-state [_ _] 662 | (f/text "Your name" owner [:name] :placeholder "Type your name here")))) 663 | ``` 664 | 665 | You can also store validation data in local state which may be useful even if you store the actual data in an atom. 666 | 667 | A slightly more complete example: [source](https://github.com/bilus/om-reforms/tree/master/examples/local_state/) [demo](http://bilus.github.io/reforms/examples/om/local_state/index.html) 668 | 669 | ** This is an experimental feature. Please report any bugs. ** 670 | 671 | 672 | ### [API Reference](http://bilus.github.io/reforms/doc/) 673 | 674 | 675 | Please feel free to tweet me @martinbilski or drop me an email: gyamtso at gmail dot com. 676 | 677 | ### TBD 678 | 679 | - Keep Readme short, move most of it to wiki. 680 | - Contact library authors. 681 | - Add tabs. Update 'controls' example. Blog post. 682 | - Port tests. 683 | 684 | ### Credits 685 | 686 | [Aspasia Beneti](https://github.com/aspasia) is the author and maintainer of [Rum bindings](https://github.com/aspasia/rum-reforms) for Reforms. 687 | 688 | ### License 689 | 690 | Copyright © 2015 Designed.ly, Marcin Bilski 691 | 692 | The use and distribution terms for this software are covered by the 693 | Eclipse Public License which can be found in the file LICENSE at the root of this distribution. 694 | By using this software in any fashion, you are agreeing to be bound by the terms of this license. 695 | You must not remove this notice, or any other, from this software. 696 | --------------------------------------------------------------------------------