├── .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 | ![](https://github.com/kwladyka/form-validator-cljs/workflows/master%20tests/badge.svg) 2 | ![](https://github.com/kwladyka/form-validator-cljs/workflows/doc%20-%3E%20gh-pages/badge.svg) 3 | ![](https://github.com/kwladyka/form-validator-cljs/workflows/clojars/badge.svg) 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 | [![Clojars Project](https://img.shields.io/clojars/v/kwladyka/form-validator-cljs.svg)](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 | --------------------------------------------------------------------------------