├── .github
└── workflows
│ ├── clojars.yaml
│ └── tests.yaml
├── .gitignore
├── LICENSE
├── README.md
├── deps.edn
├── pom.xml
├── src
└── form_validator
│ └── core.cljs
└── test
└── form_validator
├── core_test.cljs
└── test_runner.cljs
/.github/workflows/clojars.yaml:
--------------------------------------------------------------------------------
1 | name: clojars
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | clojars-deploy:
10 | runs-on: ubuntu-18.04
11 | steps:
12 | # useful for Continuous Deployment developing
13 | - name: validate if it is refs/tags/
14 | if: startsWith(github.ref,'refs/tags/') != true
15 | run: exit 1
16 |
17 | - name: checkout
18 | uses: actions/checkout@v1
19 | with:
20 | fetch-depth: 1
21 |
22 | - name: Set TAG value from refs/tags/TAG
23 | run: echo ::set-env name=GIT_TAG::${GITHUB_REF#refs/tags/}
24 |
25 | - name: Install java
26 | uses: actions/setup-java@v1
27 | with:
28 | java-version: '13.0.1'
29 |
30 | - name: Install clojure
31 | uses: DeLaGuardo/setup-clojure@2.0
32 | with:
33 | tools-deps: '1.10.1.478'
34 |
35 | - name: Tests
36 | run: clojure -A:test:test-once
37 |
38 | - name: Overwrite pom.xml
39 | run: |
40 | sed -i 's;;${{ env.GIT_TAG }};' pom.xml
41 | sed -i 's;;${{ github.sha }};' pom.xml
42 |
43 | - name: Update pom.xml
44 | run: clojure -Spom
45 |
46 | - name: Debug pom.xml
47 | run: cat pom.xml
48 |
49 | - name: Build jar
50 | run : clojure -A:depstar -m hf.depstar.jar form-validation-cljs.jar -v
51 |
52 | - name: Deploy to Clojars
53 | env:
54 | CLOJARS_USERNAME: ${{ secrets.CLOJARS_USERNAME }}
55 | CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }}
56 | run: clojure -A:clojars-deploy
57 |
58 | - name: Slack notifications
59 | uses: 8398a7/action-slack@v2
60 | with:
61 | status: ${{ job.status }}
62 | author_name: GitHub Actions
63 | env:
64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
66 | if: always()
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: master tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | tests:
10 | runs-on: ubuntu-18.04
11 | steps:
12 | - name: checkout
13 | uses: actions/checkout@v1
14 | with:
15 | fetch-depth: 1
16 |
17 | - name: Install java
18 | uses: actions/setup-java@v1
19 | with:
20 | java-version: '13.0.1'
21 |
22 | - name: Install clojure
23 | uses: DeLaGuardo/setup-clojure@2.0
24 | with:
25 | tools-deps: '1.10.1.478'
26 |
27 | - name: Tests
28 | run: clojure -A:test:test-once
29 |
30 | - name: Slack notifications
31 | uses: 8398a7/action-slack@v2
32 | with:
33 | status: ${{ job.status }}
34 | author_name: GitHub Actions
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
38 | if: always()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | /.cpcache
11 | /cljs-test-runner-out
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Krzysztof Władyka
2 |
3 | Eclipse Public License - v 2.0
4 |
5 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
6 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
7 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
8 |
9 | 1. DEFINITIONS
10 |
11 | "Contribution" means:
12 |
13 | a) in the case of the initial Contributor, the initial content
14 | Distributed under this Agreement, and
15 |
16 | b) in the case of each subsequent Contributor:
17 | i) changes to the Program, and
18 | ii) additions to the Program;
19 | where such changes and/or additions to the Program originate from
20 | and are Distributed by that particular Contributor. A Contribution
21 | "originates" from a Contributor if it was added to the Program by
22 | such Contributor itself or anyone acting on such Contributor's behalf.
23 | Contributions do not include changes or additions to the Program that
24 | are not Modified Works.
25 |
26 | "Contributor" means any person or entity that Distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which
29 | are necessarily infringed by the use or sale of its Contribution alone
30 | or when combined with the Program.
31 |
32 | "Program" means the Contributions Distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement
36 | or any Secondary License (as applicable), including Contributors.
37 |
38 | "Derivative Works" shall mean any work, whether in Source Code or other
39 | form, that is based on (or derived from) the Program and for which the
40 | editorial revisions, annotations, elaborations, or other modifications
41 | represent, as a whole, an original work of authorship.
42 |
43 | "Modified Works" shall mean any work in Source Code or other form that
44 | results from an addition to, deletion from, or modification of the
45 | contents of the Program, including, for purposes of clarity any new file
46 | in Source Code form that contains any contents of the Program. Modified
47 | Works shall not include works that contain only declarations,
48 | interfaces, types, classes, structures, or files of the Program solely
49 | in each case in order to link to, bind by name, or subclass the Program
50 | or Modified Works thereof.
51 |
52 | "Distribute" means the acts of a) distributing or b) making available
53 | in any manner that enables the transfer of a copy.
54 |
55 | "Source Code" means the form of a Program preferred for making
56 | modifications, including but not limited to software source code,
57 | documentation source, and configuration files.
58 |
59 | "Secondary License" means either the GNU General Public License,
60 | Version 2.0, or any later versions of that license, including any
61 | exceptions or additional permissions as identified by the initial
62 | Contributor.
63 |
64 | 2. GRANT OF RIGHTS
65 |
66 | a) Subject to the terms of this Agreement, each Contributor hereby
67 | grants Recipient a non-exclusive, worldwide, royalty-free copyright
68 | license to reproduce, prepare Derivative Works of, publicly display,
69 | publicly perform, Distribute and sublicense the Contribution of such
70 | Contributor, if any, and such Derivative Works.
71 |
72 | b) Subject to the terms of this Agreement, each Contributor hereby
73 | grants Recipient a non-exclusive, worldwide, royalty-free patent
74 | license under Licensed Patents to make, use, sell, offer to sell,
75 | import and otherwise transfer the Contribution of such Contributor,
76 | if any, in Source Code or other form. This patent license shall
77 | apply to the combination of the Contribution and the Program if, at
78 | the time the Contribution is added by the Contributor, such addition
79 | of the Contribution causes such combination to be covered by the
80 | Licensed Patents. The patent license shall not apply to any other
81 | combinations which include the Contribution. No hardware per se is
82 | licensed hereunder.
83 |
84 | c) Recipient understands that although each Contributor grants the
85 | licenses to its Contributions set forth herein, no assurances are
86 | provided by any Contributor that the Program does not infringe the
87 | patent or other intellectual property rights of any other entity.
88 | Each Contributor disclaims any liability to Recipient for claims
89 | brought by any other entity based on infringement of intellectual
90 | property rights or otherwise. As a condition to exercising the
91 | rights and licenses granted hereunder, each Recipient hereby
92 | assumes sole responsibility to secure any other intellectual
93 | property rights needed, if any. For example, if a third party
94 | patent license is required to allow Recipient to Distribute the
95 | Program, it is Recipient's responsibility to acquire that license
96 | before distributing the Program.
97 |
98 | d) Each Contributor represents that to its knowledge it has
99 | sufficient copyright rights in its Contribution, if any, to grant
100 | the copyright license set forth in this Agreement.
101 |
102 | e) Notwithstanding the terms of any Secondary License, no
103 | Contributor makes additional grants to any Recipient (other than
104 | those set forth in this Agreement) as a result of such Recipient's
105 | receipt of the Program under the terms of a Secondary License
106 | (if permitted under the terms of Section 3).
107 |
108 | 3. REQUIREMENTS
109 |
110 | 3.1 If a Contributor Distributes the Program in any form, then:
111 |
112 | a) the Program must also be made available as Source Code, in
113 | accordance with section 3.2, and the Contributor must accompany
114 | the Program with a statement that the Source Code for the Program
115 | is available under this Agreement, and informs Recipients how to
116 | obtain it in a reasonable manner on or through a medium customarily
117 | used for software exchange; and
118 |
119 | b) the Contributor may Distribute the Program under a license
120 | different than this Agreement, provided that such license:
121 | i) effectively disclaims on behalf of all other Contributors all
122 | warranties and conditions, express and implied, including
123 | warranties or conditions of title and non-infringement, and
124 | implied warranties or conditions of merchantability and fitness
125 | for a particular purpose;
126 |
127 | ii) effectively excludes on behalf of all other Contributors all
128 | liability for damages, including direct, indirect, special,
129 | incidental and consequential damages, such as lost profits;
130 |
131 | iii) does not attempt to limit or alter the recipients' rights
132 | in the Source Code under section 3.2; and
133 |
134 | iv) requires any subsequent distribution of the Program by any
135 | party to be under a license that satisfies the requirements
136 | of this section 3.
137 |
138 | 3.2 When the Program is Distributed as Source Code:
139 |
140 | a) it must be made available under this Agreement, or if the
141 | Program (i) is combined with other material in a separate file or
142 | files made available under a Secondary License, and (ii) the initial
143 | Contributor attached to the Source Code the notice described in
144 | Exhibit A of this Agreement, then the Program may be made available
145 | under the terms of such Secondary Licenses, and
146 |
147 | b) a copy of this Agreement must be included with each copy of
148 | the Program.
149 |
150 | 3.3 Contributors may not remove or alter any copyright, patent,
151 | trademark, attribution notices, disclaimers of warranty, or limitations
152 | of liability ("notices") contained within the Program from any copy of
153 | the Program which they Distribute, provided that Contributors may add
154 | their own appropriate notices.
155 |
156 | 4. COMMERCIAL DISTRIBUTION
157 |
158 | Commercial distributors of software may accept certain responsibilities
159 | with respect to end users, business partners and the like. While this
160 | license is intended to facilitate the commercial use of the Program,
161 | the Contributor who includes the Program in a commercial product
162 | offering should do so in a manner which does not create potential
163 | liability for other Contributors. Therefore, if a Contributor includes
164 | the Program in a commercial product offering, such Contributor
165 | ("Commercial Contributor") hereby agrees to defend and indemnify every
166 | other Contributor ("Indemnified Contributor") against any losses,
167 | damages and costs (collectively "Losses") arising from claims, lawsuits
168 | and other legal actions brought by a third party against the Indemnified
169 | Contributor to the extent caused by the acts or omissions of such
170 | Commercial Contributor in connection with its distribution of the Program
171 | in a commercial product offering. The obligations in this section do not
172 | apply to any claims or Losses relating to any actual or alleged
173 | intellectual property infringement. In order to qualify, an Indemnified
174 | Contributor must: a) promptly notify the Commercial Contributor in
175 | writing of such claim, and b) allow the Commercial Contributor to control,
176 | and cooperate with the Commercial Contributor in, the defense and any
177 | related settlement negotiations. The Indemnified Contributor may
178 | participate in any such claim at its own expense.
179 |
180 | For example, a Contributor might include the Program in a commercial
181 | product offering, Product X. That Contributor is then a Commercial
182 | Contributor. If that Commercial Contributor then makes performance
183 | claims, or offers warranties related to Product X, those performance
184 | claims and warranties are such Commercial Contributor's responsibility
185 | alone. Under this section, the Commercial Contributor would have to
186 | defend claims against the other Contributors related to those performance
187 | claims and warranties, and if a court requires any other Contributor to
188 | pay any damages as a result, the Commercial Contributor must pay
189 | those damages.
190 |
191 | 5. NO WARRANTY
192 |
193 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
194 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
195 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
196 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
197 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
198 | PURPOSE. Each Recipient is solely responsible for determining the
199 | appropriateness of using and distributing the Program and assumes all
200 | risks associated with its exercise of rights under this Agreement,
201 | including but not limited to the risks and costs of program errors,
202 | compliance with applicable laws, damage to or loss of data, programs
203 | or equipment, and unavailability or interruption of operations.
204 |
205 | 6. DISCLAIMER OF LIABILITY
206 |
207 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
208 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
209 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
210 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
211 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
212 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
213 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
214 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
215 | POSSIBILITY OF SUCH DAMAGES.
216 |
217 | 7. GENERAL
218 |
219 | If any provision of this Agreement is invalid or unenforceable under
220 | applicable law, it shall not affect the validity or enforceability of
221 | the remainder of the terms of this Agreement, and without further
222 | action by the parties hereto, such provision shall be reformed to the
223 | minimum extent necessary to make such provision valid and enforceable.
224 |
225 | If Recipient institutes patent litigation against any entity
226 | (including a cross-claim or counterclaim in a lawsuit) alleging that the
227 | Program itself (excluding combinations of the Program with other software
228 | or hardware) infringes such Recipient's patent(s), then such Recipient's
229 | rights granted under Section 2(b) shall terminate as of the date such
230 | litigation is filed.
231 |
232 | All Recipient's rights under this Agreement shall terminate if it
233 | fails to comply with any of the material terms or conditions of this
234 | Agreement and does not cure such failure in a reasonable period of
235 | time after becoming aware of such noncompliance. If all Recipient's
236 | rights under this Agreement terminate, Recipient agrees to cease use
237 | and distribution of the Program as soon as reasonably practicable.
238 | However, Recipient's obligations under this Agreement and any licenses
239 | granted by Recipient relating to the Program shall continue and survive.
240 |
241 | Everyone is permitted to copy and distribute copies of this Agreement,
242 | but in order to avoid inconsistency the Agreement is copyrighted and
243 | may only be modified in the following manner. The Agreement Steward
244 | reserves the right to publish new versions (including revisions) of
245 | this Agreement from time to time. No one other than the Agreement
246 | Steward has the right to modify this Agreement. The Eclipse Foundation
247 | is the initial Agreement Steward. The Eclipse Foundation may assign the
248 | responsibility to serve as the Agreement Steward to a suitable separate
249 | entity. Each new version of the Agreement will be given a distinguishing
250 | version number. The Program (including Contributions) may always be
251 | Distributed subject to the version of the Agreement under which it was
252 | received. In addition, after a new version of the Agreement is published,
253 | Contributor may elect to Distribute the Program (including its
254 | Contributions) under the new version.
255 |
256 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
257 | receives no rights or licenses to the intellectual property of any
258 | Contributor under this Agreement, whether expressly, by implication,
259 | estoppel or otherwise. All rights in the Program not expressly granted
260 | under this Agreement are reserved. Nothing in this Agreement is intended
261 | to be enforceable by any entity that is not a Contributor or Recipient.
262 | No third-party beneficiary rights are created under this Agreement.
263 |
264 | Exhibit A - Form of Secondary Licenses Notice
265 |
266 | "This Source Code may also be made available under the following
267 | Secondary Licenses when the conditions for such availability set forth
268 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
269 | version(s), and exceptions or additional permissions here}."
270 |
271 | Simply including a copy of this Agreement, including this Exhibit A
272 | is not sufficient to license the Source Code under Secondary Licenses.
273 |
274 | If it is not possible or desirable to put the notice in a particular
275 | file, then You may include the notice in a location (such as a LICENSE
276 | file in a relevant directory) where a recipient would be likely to
277 | look for such a notice.
278 |
279 | You may add additional accurate notices of copyright ownership.
280 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 | # form-validator
6 |
7 | ClojureScript library to validate forms.
8 |
9 | ## Rationale
10 |
11 | - Move repeatable code for form validation from app to library.
12 | - Validate by `spec` and `fn`.
13 | - Custom messages. Could be `"foo"`, `{:level :warn :msg "foo"}` or whatever.
14 | - Custom workflow. Let you choose when to show messages: `on-blur` / `on-change` / immediately after load page / ...
15 | - Easy and simple independent small solution. Compatible with `re-frame`, `fulcro` or whatever.
16 | - Work with different types of inputs, also custom ones like in material UI.
17 | - Base logic to make custom UI, but no UI included. No limitations.
18 |
19 | Why? I need it myself. But I didn't find any library which satisfy me, so I wrote my own.
20 |
21 | Read my article [form validation](https://clojure.wladyka.eu/posts/form-validation/) to learn more rationales.
22 |
23 | ## Tutorial and Demo
24 |
25 | Discover it naturally by real code: https://kwladyka.github.io/form-validator-cljs/
26 |
27 |
28 |
29 | Please keep in mind it is an example. It could easy take actions on different events `on-change` / `on-blur` / button click. All is your choice.
30 |
31 | ## Add dependency
32 |
33 | [](https://clojars.org/kwladyka/form-validator-cljs)
34 |
35 | ### Require in ns
36 |
37 | ```clojure
38 | (:require [form-validator.core :as form-validator])
39 | ```
40 |
41 | ### Only if you use reagent
42 |
43 | To be compatible with reagent library, needs to use `reagent.core/atom` instead `clojure.core/atom`.
44 |
45 | ```clojure
46 | (ns app.core
47 | (:require [reagent.core :as r]
48 | [form-validator.core :as form-validator]))
49 |
50 | ;; First line in core ns or dedicated init fn is a right place
51 | (swap! form-validator/conf #(merge % {:atom r/atom}))
52 | ```
53 |
54 | ## TL;DR
55 |
56 | Init form
57 |
58 | ```clojure
59 | (-> {:names->value {:email ""
60 | :password ""}
61 | :form-spec ::spec/form
62 | (form-validator/init-form))
63 | ```
64 |
65 | return `atom` contained map:
66 |
67 | ```clojure
68 | {:form-spec :app.spec/form
69 | :names->value {:email "" :password ""}
70 | :names->invalid {:email [:app.spec/form :app.spec/email] :password [:app.spec/form :app.spec/password :app.spec/password-not-empty]}
71 | :names->show #{}
72 | :names->validators {}}
73 | ```
74 |
75 | Then you can use functions from ns `form-validator.core`:
76 |
77 | - `event->names->value!` - With `on-change` / `on-blur` input event to update values.
78 | - `event->show-message` - With `on-blur` / `on-change` input event to trigger when show messages in UI.
79 | - `?show-message` - Get message to show in UI for input. Also to know if mark input as not valid in UI.
80 | - `form-valid?` - true / false
81 | - `validate-form-and-show?` - Call `validate-form` and show all messages. Use with submit button.
82 |
83 | ## Specification
84 |
85 | ### Init form
86 |
87 | ```clojure
88 |
89 | ;; clojure.spec.alpha
90 |
91 | (s/def ::email (s/and string? (partial re-matches #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")))
92 |
93 | (s/def ::password-not-empty not-empty)
94 | (s/def ::password-length #(<= 6 (count %)))
95 | (s/def ::password (s/and string? ::password-not-empty ::password-length))
96 |
97 | (s/def ::form (s/keys :req-un [::email ::password]
98 | :opt-un [::password-repeat]))
99 |
100 | ;; form-valiadtor
101 |
102 | (-> {:names->value {:email ""
103 | :password ""}
104 | :form-spec ::form
105 | :names->validators {:email [email-exist?]
106 | :password-repeat [password-repeat? ::spec-key]}}
107 | (form-validator/init-form))
108 | ```
109 |
110 | - `:names->value` - Form inputs with values to initialize.
111 | Use cases: Empty values for new data form / filled values with already existed data (update form) / if input is not required by spec `:opt-un` it can be ommited.
112 | - `:form-spec` - Spec to validate whole form.
113 | Should use always, unless you don't have specs.
114 | - `:names->validators` - Vector of spec keywords and fn. Order matter.
115 | Use cases: When don't have `spec` for form / if checkbox "accept terms" is checked / `fn` to compare password-repeat / check if user already exist by API during registration.
116 | - `:names->show` - `#{}` with names of inputs to show error messages on start.
117 | Use cases: Form with already filled values.
118 |
119 | You can use `:form-spec` and `:names->validators` together. `:form-spec` is checked first.
120 |
121 | ### Interact with form
122 |
123 | `(init-form ...)` return `atom`:
124 |
125 | ```clojure
126 | {:form-spec :app.spec/form
127 | :names->value {:email "", :password ""}
128 | :names->invalid {:email [::form ::form-map ::email]
129 | :password [::form ::form-map ::password ::password-not-empty]}
130 | :names->show #{}
131 | :names->validators {:email #object[cljs$core$sp1], :password-repeat #object[cljs$core$sp1]}}
132 | ```
133 |
134 | - `:form-spec` - Init form value without any change.
135 | - `:names->value` - Values of the form.
136 | - `:names->invalid` - Invalid inputs with reasons of validation fail.
137 | - `:names->show` - Add name of the input here, when you want to show message in UI.
138 | - `:names->validators` - All validators converted to one fn which works similar to `some`. Check all validators for specific input one by one, unless fail or return `nil`.
139 |
140 | ### Messages
141 |
142 | - `:names->validators` can contain `::spec-key` and `fn`.
143 | - Spec always return vector of `:cljs.spec.alpha/problems` `:via`. For example `[::form ::form-map ::password ::password-not-empty]`. It means spec `::form` refer to spec `::form-map`, which refer to spec `::password`, which refer to spec `::password-not-empty`, which failed.
144 | - `fn` can return vectors like spec, but also strings, map or any value.
145 | ```clojure
146 | ;; Check error for input name "password"
147 | (->> {::email "Typo? It doesn't look valid."
148 | ::password "Minimum 6 characters and one special character !@#$%^&*."
149 | :password-not-equal "Repeat password has to be the same."}
150 | (form-validator/?show-message form :password))
151 | ```
152 | Based on `[::form ::form-map ::password ::password-not-empty]` it is trying to find `::password-not-empty` message. Map not contain message for this spec. Then try to find `::password` and return message. If not find, going deeper. If not find any, return `true`.
153 |
154 | If reason of fail is not a vector, then it is returning as it is. For example `"cutom message"` or `{:level :warn :msg "This is only warning."}`. This is dedicated for `fn` validators.
155 |
156 | ## Tips & Tricks & FAQ
157 |
158 | - Architecture of library let you make custom UI and validation on it. You can modify `atom` returned by `form-init`, `add-watch` on `atom`, add functions on top of core functions, use your own functions instead of core ones. It is designed to let you make custom things. In most of cases you really don't need to do it. It could be useful if you want to make your module based on this one.
159 | - To not prevent send form with warning (not error) messages like "Password is weak. We recommend to use better password" you have to use your own `form-valid?` function or make two `form-init` (first for errors and second for warnings). I decided to not make it as part of this library, because it is individual thing for project.
160 | - You want to write your own functions to generate UI HTML form and inputs based on this library. UI is individual thing for project, so I decided it wouldn't be part of this library. Instead this library give solid basement, which let you to build visualisation on it.
161 |
162 | ---
163 |
164 | Everything below this line is mainly for myself as a maintainer of this library.
165 |
166 | ## Developing
167 |
168 | Library has to be always check with web browsers manually! Not only automated tests. The reasons are differences between web browsers and practical aspects of usability vs imagination :)
169 |
170 | To do it use `doc` branch from this repository.
171 |
172 | After all make a commit to readme with new sha hash for deps.edn.
173 |
174 | ### Tests
175 |
176 | `clj -A:test:test-once`
177 |
178 | `clj -A:test:test-watch`
179 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"}
2 | org.clojure/clojurescript {:mvn/version "1.10.520"}}
3 | :paths ["src"]
4 | :aliases {:test {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}
5 | re-frame {:mvn/version "0.11.0-rc1"}}
6 | :extra-paths ["test"]}
7 | :test-once {:main-opts ["-m" "cljs-test-runner.main"]}
8 | :test-watch {:main-opts ["-m" "cljs-test-runner.main" "-w" "src"]}
9 | :depstar {:extra-deps {seancorfield/depstar {:mvn/version "0.3.4"}}}
10 | :clojars-deploy {:extra-deps {deps-deploy {:mvn/version "0.0.9"}}
11 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "form-validation-cljs.jar" "true"]}}}
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | kwladyka
5 | form-validator-cljs
6 |
7 | form-validator-cljs
8 | https://github.com/kwladyka/form-validator-cljs
9 | ClojureScript library to validate forms.
10 |
11 |
12 |
13 | Eclipse Public License 2.0
14 | https://github.com/kwladyka/form-validator-cljs/blob/master/LICENSE
15 | repo
16 |
17 |
18 |
19 |
20 |
21 | Krzysztof Władyka
22 |
23 |
24 |
25 |
26 | https://github.com/kwladyka/form-validator-cljs
27 | scm:git:git://github.com/kwladyka/form-validator-cljs.git
28 | scm:git:ssh://git@github.com/kwladyka/form-validator-cljs.git
29 |
30 |
31 |
32 |
33 |
34 | clojars
35 | https://repo.clojars.org/
36 |
37 |
38 |
39 |
40 | clojars
41 | Clojars repository
42 | https://clojars.org/repo
43 |
44 |
45 |
46 |
47 | src
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/form_validator/core.cljs:
--------------------------------------------------------------------------------
1 | (ns form-validator.core
2 | (:require [cljs.spec.alpha :as s]))
3 |
4 | (def conf (atom {:atom atom}))
5 |
6 | (defn ?spec-problems
7 | "Return nil if pass."
8 | [spec value]
9 | (-> (s/explain-data spec value)
10 | :cljs.spec.alpha/problems))
11 |
12 | (defn spec-validate
13 | "Check value by spec.
14 | If validate return nil, otherwise return a reason: the :via value of spec problem."
15 | [form spec name]
16 | (->> (get-in @form [:names->value name])
17 | (?spec-problems spec)
18 | (first)
19 | :via))
20 |
21 | (defn validator->fn
22 | "If spec, transform to fn.
23 | Make consistent fn to check values:
24 | (fn [name] ...) return reason of fail or nil"
25 | [form validator]
26 | (if (fn? validator)
27 | (partial validator form)
28 | (partial spec-validate form validator)))
29 |
30 | (defn validators->some-validators
31 | "convert validators {:name [::spec f ...]} into {:name some-validator} which check all validators unless one of them fail."
32 | [form names->validators]
33 | (reduce-kv (fn [m name validators]
34 | (assoc m name (->> (map (partial validator->fn form) validators)
35 | (apply some-fn))))
36 | {} names->validators))
37 |
38 | (defn validate-name
39 | "Validate name (input) in names->value.
40 | Update names->invalid."
41 | [form name]
42 | (when-let [some-validators (get-in @form [:names->validators name])]
43 | (if-let [?invalid (some-validators name)]
44 | (swap! form #(assoc-in % [:names->invalid name] ?invalid))
45 | (swap! form #(update % :names->invalid (fn [names->invalid]
46 | (dissoc names->invalid name)))))))
47 |
48 | (defn validate-form
49 | "1. Validate names->value with :spec-form.
50 | 2. Next validate names->value with names->validators.
51 | Do not overwrite errors from 1. by 2."
52 | [form]
53 | (swap! form #(assoc % :names->invalid {}))
54 | (doseq [{:keys [in via]} (?spec-problems (:form-spec @form) (:names->value @form))]
55 | (let [name (first in)]
56 | (swap! form #(assoc-in % [:names->invalid name] via))))
57 | (doseq [[name value] (:names->value @form)]
58 | (when-not (get-in @form [:names->invalid name])
59 | (validate-name form name))))
60 |
61 | (defn event->names->value!
62 | "Update input value to names->value and validate.
63 | The best with :on-change event."
64 | [form event]
65 | (let [name (keyword event.target.name)
66 | type event.target.type
67 | value (case type
68 | "checkbox" (if event.target.checked
69 | (or (not-empty event.target.value) true)
70 | false)
71 | event.target.value)]
72 | (swap! form #(assoc-in % [:names->value name] value))
73 | (validate-form form)))
74 |
75 | (defn show-if-not-empty
76 | "Add name (input) to :names->show if value is not empty.
77 | hint: Add to :names->show has to be done once and it stays forever.
78 | Prevent to show errors when user jump between inputs by tab."
79 | [form name]
80 | (let [value (get-in @form [:names->value name])]
81 | (when-not (or (nil? value)
82 | (= "" value))
83 | (swap! form #(update % :names->show (fn [names->show]
84 | (conj names->show name)))))))
85 |
86 | (defn event->show-message [form event]
87 | (->> (keyword event.target.name)
88 | (show-if-not-empty form)))
89 |
90 | (defn show-all
91 | "Add all names (inputs) to :names->show"
92 | [form]
93 | (swap! form #(assoc % :names->show (-> (concat (keys (:names->value @form)) (keys (:names->invalid @form)))
94 | (set)))))
95 |
96 | (defn get-message
97 | "1. If invalid is a vector find the deepest message.
98 | 2. If invalid is not a vector return as it is."
99 | [form name messages]
100 | (when-let [invalid-reasons (get-in @form [:names->invalid name])]
101 | (if (vector? invalid-reasons)
102 | (->> (reverse invalid-reasons)
103 | (some messages))
104 | invalid-reasons)))
105 |
106 | (defn ?show-message
107 | "1. If parameter messages is provided return a message.
108 | If message is not supported, then return true.
109 | 2. If messages is not provided return boolean"
110 | ([form name]
111 | (and (contains? (:names->invalid @form) name)
112 | (contains? (:names->show @form) name)))
113 | ([form name messages]
114 | (when (?show-message form name)
115 | (or (get-message form name messages)
116 | true))))
117 |
118 | (defn form-valid? [form]
119 | (empty? (:names->invalid @form)))
120 |
121 | (defn validate-form-and-show? [form]
122 | (validate-form form)
123 | (show-all form)
124 | (form-valid? form))
125 |
126 | (defn init-form [form-conf]
127 | (let [atom (:atom @conf)
128 | form (atom {})]
129 | (reset! form {:form-spec (:form-spec form-conf)
130 | :names->value (:names->value form-conf)
131 | :names->invalid {}
132 | :names->show (or (:names->show form-conf) #{})
133 | :names->validators (validators->some-validators form (:names->validators form-conf))})
134 | (validate-form form)
135 | form))
--------------------------------------------------------------------------------
/test/form_validator/core_test.cljs:
--------------------------------------------------------------------------------
1 | (ns form-validator.core-test
2 | (:require [cljs.test :refer-macros [deftest is testing use-fixtures]]
3 | [form-validator.core :as form-validator]
4 | [cljs.spec.alpha :as s]))
5 |
6 | ;;; Validators
7 |
8 | (s/def ::checked boolean)
9 | (s/def ::email (s/and string? (partial re-matches #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")))
10 | (s/def ::password-not-empty not-empty)
11 | (s/def ::password-length #(<= 8 (count %)))
12 | (s/def ::spec-password-repeat #(= (:password %) (:password-repeat %)))
13 | (s/def ::password (s/and string? ::password-not-empty ::password-length))
14 | (s/def ::form-map (s/keys :req-un [::email ::password]
15 | :opt-un [::password-repeat]))
16 |
17 | (s/def ::foo #(= "foo" %))
18 |
19 | (s/def ::form (s/and ::form-map
20 | ::spec-password-repeat))
21 |
22 | (defn password-repeat? [form name]
23 | "Example of check depended on multiple inputs values."
24 | (let [password (get-in @form [:names->value :password])
25 | password-repeat (get-in @form [:names->value name])]
26 | (when-not (= password password-repeat)
27 | [:password-repeat :password-not-equal])))
28 |
29 | (defn email-exist? [form name]
30 | "Check third party system if user already exist."
31 | (when (= "exist@example.com" (get-in @form [:names->value name]))
32 | [:email :email-exist]))
33 |
34 | ;;; Helpers
35 |
36 | (defn event-simulation [event]
37 | #(event (clj->js {:target %})))
38 |
39 | ;;; Test
40 |
41 | (deftest init-form-test
42 | (testing "init form and first validations"
43 | (let [names->invalid (fn [form-conf]
44 | (-> @(form-validator/init-form form-conf)
45 | :names->invalid))]
46 | (is (= (-> {:names->value {:email ""
47 | :password ""}
48 | :form-spec ::form}
49 | (names->invalid))
50 | {:email [::form ::form-map ::email]
51 | :password [::form ::form-map ::password ::password-not-empty]})
52 | "correct assign ::form problems to @names->invalid.")
53 | (is (= (-> {:names->value {:email "exist@example.com"
54 | :password "qwaszx!!"
55 | :password-repeat "qwaszx"}
56 | :form-spec ::form}
57 | (names->invalid))
58 | {nil [::form ::spec-password-repeat]})
59 | "nil key mean fail is not assigned to any input name in form.
60 | ::spec-password-repeat is on the top of ::form.
61 | There is no information to which field it applies.")
62 | (is (= (-> {:names->value {:email "exist@example.com"
63 | :password "qwaszx!!"
64 | :password-repeat "qwaszx"}
65 | :form-spec ::form-map
66 | :names->validators {:email [email-exist?]
67 | :password-repeat [password-repeat?]}}
68 | (names->invalid))
69 | {:email [:email :email-exist]
70 | :password-repeat [:password-repeat :password-not-equal]})
71 | "additional validations by :names->validators")
72 | (is (= (-> {:names->value {:email "foo@example.com"
73 | :password "12345678"
74 | :password-repeat "12345678"}
75 | :form-spec ::form-map
76 | :names->validators {:email [email-exist?]
77 | :password-repeat [password-repeat?]}}
78 | (names->invalid))
79 | {})
80 | "pass"))))
81 |
82 | (deftest update-values-test
83 | (testing "Update values"
84 | (let [form (-> {:names->value {:email "foo@example.com"
85 | :password "12345678"}
86 | :form-spec ::form}
87 | (form-validator/init-form))
88 | on-change (event-simulation (partial form-validator/event->names->value! form))]
89 | (on-change {:name "email" :value "bar@example.com"})
90 | (on-change {:name "password" :value "qwaszx"})
91 | (is (= (:names->value @form)
92 | {:email "bar@example.com"
93 | :password "qwaszx"})
94 | "Update values")
95 |
96 | (on-change {:name "password-repeat" :value "12345678"})
97 | (is (= (:names->value @form)
98 | {:email "bar@example.com"
99 | :password "qwaszx"
100 | :password-repeat "12345678"})
101 | "Add new values"))))
102 |
103 | (deftest show-messages-test
104 | (testing "Show inputs messages"
105 | (let [form (-> {:names->value {:email ""
106 | :password ""}
107 | :form-spec ::form}
108 | (form-validator/init-form))
109 | on-change (event-simulation (partial form-validator/event->names->value! form))
110 | on-blur (event-simulation (partial form-validator/event->show-message form))]
111 | (on-change {:name "email" :value "foo@example.com"})
112 | (on-blur {:name "email"})
113 | (on-change {:name "password" :value "qwaszx"})
114 | (on-blur {:name "password"})
115 | (on-change {:name "password" :value ""})
116 | (on-blur {:name "password"})
117 | (on-change {:name "password-repeat" :value ""})
118 | (on-blur {:name "password-repeat"})
119 | (is (= (:names->show @form)
120 | #{:email :password})
121 | ":password-repeat value is empty so it is not consider to add to show message.
122 | :password was filed and later cleaned. Message should be shown.
123 |
124 | Situation when user use tab to switch between inputs.
125 | It is not the right moment to show message.
126 | If user had some value before, it is already added."))
127 |
128 | (let [form (->> {:names->value {:email "foo@example.com"
129 | :password ""}
130 | :form-spec ::form}
131 | (form-validator/init-form))]
132 | (form-validator/show-all form)
133 | (is (= (:names->show @form)
134 | #{:email :password})
135 | "show all")))
136 |
137 | (testing "Show message?"
138 | (let [form (-> {:names->value {:email ""}
139 | :form-spec ::form}
140 | (form-validator/init-form))
141 | on-change (event-simulation (partial form-validator/event->names->value! form))
142 | on-blur (event-simulation (partial form-validator/event->show-message form))]
143 | (is (nil? (form-validator/?show-message form :email {})))
144 | (on-change {:name "email" :value "foo"})
145 | (is (false? (form-validator/?show-message form :email)))
146 | (is (nil? (form-validator/?show-message form :email {}))
147 | "on-change, but not on-blur yet. User during first typing in input.")
148 | (on-blur {:name "email"})
149 | (is (true? (form-validator/?show-message form :email)))
150 | (is (some? (form-validator/?show-message form :email {})))
151 | (on-change {:name "email" :value "foo@example.com"})
152 | (is (nil? (form-validator/?show-message form :email {})))))
153 |
154 | (testing "Show right message with spec contained deeper spec"
155 | (let [form (->> {:names->value {:password "qwaszx"}
156 | :names->validators {:password [::password]}
157 | :names->show #{:password}}
158 | (form-validator/init-form))]
159 | (is (= "Password too short"
160 | (->> {::password "Password has to have more than 8 characters"
161 | ::password-length "Password too short"}
162 | (form-validator/?show-message form :password)))
163 | "Show last spec, because it is supported by messages")
164 | (is (= "Password has to have more than 8 characters"
165 | (->> {::password "Password has to have more than 8 characters"}
166 | (form-validator/?show-message form :password)))
167 | "Show not last spec, because last spec in not supported by messages"))
168 |
169 | (let [form (->> {:names->value {:password "qwaszx"}
170 | :names->validators {:password [(constantly "custom message")]}
171 | :names->show #{:password}}
172 | (form-validator/init-form))]
173 | (is (= "custom message"
174 | (form-validator/?show-message form :password {}))
175 | "When invalid reason in not a vector, return it. It can be also for example map."))))
176 |
177 | (deftest input-types-test
178 | (let [form (-> {:names->value {}
179 | :names->validators {:email [::email]
180 | :password [::password]
181 | :checkbox-without-value [::checked]
182 | :checkbox-with-value [::foo]}}
183 | (form-validator/init-form))
184 | get-invalid #(get-in @form [:names->invalid %])
185 | on-change (event-simulation (partial form-validator/event->names->value! form))]
186 | (testing "text"
187 | (on-change {:type "text" :name "email" :value "foo@example.com"})
188 | (is (nil? (get-invalid :email))
189 | "input pass")
190 | (on-change {:type "text" :name "email" :value "foo@example"})
191 | (is (= (get-invalid :email)
192 | [::email])
193 | "input fail"))
194 |
195 | (testing "password"
196 | (on-change {:type "password" :name "password" :value "12345678"})
197 | (is (nil? (get-invalid :password))
198 | "password pass")
199 | (on-change {:type "password" :name "password" :value ""})
200 | (is (= (get-invalid :password)
201 | [::password ::password-not-empty])
202 | "password fail"))
203 |
204 | (testing "checkbox"
205 | (on-change {:type "checkbox" :checked true :name "checkbox-without-value"})
206 | (is (nil? (get-invalid :checkbox-without-value))
207 | "checkbox-without-value pass")
208 | (on-change {:type "checkbox" :checked false :name "checkbox-without-value"})
209 | (is (= (get-invalid :checkbox-without-value)
210 | [::checked])
211 | "checkbox-without-value fail")
212 | (on-change {:type "checkbox" :checked true :name "checkbox-with-value" :value "foo"})
213 | (is (nil? (get-invalid :checkbox-with-value))
214 | "checkbox-with-value pass")
215 | (on-change {:type "checkbox" :checked false :name "checkbox-with-value" :value "foo"})
216 | (is (= (get-invalid :checkbox-with-value)
217 | [::foo])
218 | "checkbox-with-value fail"))))
--------------------------------------------------------------------------------
/test/form_validator/test_runner.cljs:
--------------------------------------------------------------------------------
1 | (ns form-validator.test-runner
2 | (:require [form-validator.core-test]))
3 |
--------------------------------------------------------------------------------