├── .babelrc
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .npmignore
├── License.txt
├── README.md
├── babel.config.js
├── dev
└── input-validator-example.jsx
├── example
├── bundle.js
├── bundle.js.map
├── css
│ └── styles.css
├── dev.jsx
├── fonts
│ ├── rw-widgets.eot
│ ├── rw-widgets.svg
│ ├── rw-widgets.ttf
│ └── rw-widgets.woff
├── index.html
└── input-validator-example.jsx
├── gulpfile.js
├── lib
├── Message.js
├── MessageContainer.js
├── MessageTrigger.js
├── Validator.js
├── connectToMessageContainer.js
├── index.js
├── package.json
├── shallowEqual.js
└── util
│ └── babelHelpers.js
├── package-lock.json
├── package.json
├── src
├── ChildBridge.jsx
├── Message.js
├── MessageContainer.js
├── MessageTrigger.js
├── Validator.js
├── connectToMessageContainer.js
├── index.js
└── shallowEqual.js
├── test
├── connect.jsx
├── container.jsx
├── trigger.jsx
└── validator.js
└── webpack
└── example-config.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-syntax-object-rest-spread",
7 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
8 | ["@babel/plugin-proposal-class-properties", { "loose": true}],
9 | "@babel/plugin-syntax-dynamic-import"
10 | ]
11 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "mocha": true
8 | },
9 | "globals": {
10 | "sinon": true,
11 | "chai": true
12 | },
13 | "plugins": [
14 | "eslint-plugin-react"
15 | ],
16 | "rules": {
17 | "no-eval": 2,
18 | "strict": 0,
19 | "eol-last": 0,
20 | "dot-notation": [2, { "allowKeywords": true }],
21 | "semi": [0, "never"],
22 | "curly": 0,
23 | "eqeqeq": [2, "allow-null"],
24 | "no-undef": 2,
25 | "quotes": [2, "single", "avoid-escape"],
26 | "no-trailing-spaces": 0,
27 | "no-underscore-dangle": 0,
28 | "no-multi-spaces": 0,
29 | "no-mix-requires": 0,
30 | "no-unused-expressions": 0,
31 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
32 | "no-use-before-define": 0,
33 | "key-spacing": 0,
34 | "return-no-assign": 0,
35 | "consitent-return": 0,
36 | "no-shadow": 0,
37 | "no-sequences": 0,
38 | "react/jsx-no-undef": 1,
39 | "react/jsx-uses-react": 1,
40 | "react/jsx-uses-vars": 1,
41 | "react/react-in-jsx-scope": 1,
42 | "react/self-closing-comp": 1,
43 | "semi-spaceing": 0
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | # =========================
31 | # Operating System Files
32 | # =========================
33 |
34 | # OSX
35 | # =========================
36 |
37 | .DS_Store
38 | .AppleDouble
39 | .LSOverride
40 |
41 | # Icon must end with two \r
42 | Icon
43 |
44 |
45 | # Thumbnails
46 | ._*
47 |
48 | # Files that might appear on external disk
49 | .Spotlight-V100
50 | .Trashes
51 |
52 | # Directories potentially created on remote AFP share
53 | .AppleDB
54 | .AppleDesktop
55 | Network Trash Folder
56 | Temporary Items
57 | .apdisk
58 |
59 | # Windows
60 | # =========================
61 |
62 | # Windows image file caches
63 | Thumbs.db
64 | ehthumbs.db
65 |
66 | # Folder config file
67 | Desktop.ini
68 |
69 | # Recycle Bin used on file shares
70 | $RECYCLE.BIN/
71 |
72 | # Windows Installer files
73 | *.cab
74 | *.msi
75 | *.msm
76 | *.msp
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .npmignore
3 |
4 | src/
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Jason Quense
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-input-message
2 |
3 | A small, completely unopinionated way, to display messages next to inputs based on events.
4 | Helpful for displaying input validation messages.
5 |
6 | There is a frustrating trend in javascript form validation solutions that couple the view concerns of a form
7 | (hiding/showing of messages) with some specific data layer model, or abstraction.
8 | This often means that in order to use a form validator you also need to use a specific js schema validator,
9 | or are tied into using a specific validation library. `react-input-message` strives to
10 | provide _just_ a solution to quickly and easily annotating form controls without requiring that you use a
11 | specific validation or data schema library.
12 |
13 | ## Install
14 |
15 | ```sh
16 | npm i -S react-input-message
17 | ```
18 |
19 | **Depends on the `Promise` global object** Most browsers and versions of node
20 | already support this but for lder browsers please provide a polyfill
21 |
22 | ## Use
23 |
24 | You render your inputs as you normally would, except that you wrap them inside a `MessageTrigger`
25 | component which will watch its child input for events.
26 |
27 | ```js
28 | render(){
29 | var messages = {
30 | name: ['name is required']
31 | }
32 |
33 | return (
34 |
38 |
61 |
62 | )
63 | ```
64 |
65 |
66 | `react-input-message` exports 3 simple components and a utility class:
67 |
68 | ### `MessageContainer`
69 |
70 | __Props__
71 |
72 | #### `onValidationNeeded({ fields: array, type: string, args: ?any })`
73 |
74 | A handler that fires for each `MessageTrigger` component with a `for` prop
75 |
76 | #### `messages: object`
77 |
78 | A hash of unique names (`for` prop values) and either a string, or an array of strings.
79 |
80 | #### `passthrough: bool`
81 |
82 | Allow a nested Container to receive the messages of a parent container.
83 |
84 | #### `mapNames(names : array) -> array`
85 |
86 | A mapping operation on the inner container names, to the outer container.
87 |
88 | #### `mapMessages(messages: object) -> object`
89 |
90 | A mapping operation on the outer container messages, to the inner container messages.
91 |
92 | ### `MessageTrigger`
93 |
94 | A MessageTrigger is a component that listens to its child for events and triggers a
95 | validation event in the containing `MessageContainer`. Generally this will be an input component.
96 |
97 |
98 | #### `for: string | array object`
111 |
112 | A function that is passed the child, `active` boolean. returns an object of props to add to the child.
113 |
114 | ```js
115 | function inject(child, isActive){
116 | return {
117 | className: classnames(child.props.className, {
118 | 'message-error': isActive
119 | })
120 | }
121 | }
122 |
123 |
124 | ```
125 |
126 | #### `events: string | array` default: `'onChange'`
127 |
128 | An array of prop handlers that the MessageTrigger will list on,
129 | and trigger a `onValidationNeeded` event in the Container
130 |
131 | Leaving the `for` prop `undefined` is a good way to create buttons that can trigger validation for a
132 | group (or the entire container), but will not be the subject of a validation itself.
133 |
134 | #### `Message`
135 |
136 | Displays the actual messages for a field, the default implementation just concats the messages together with `, `
137 | but you can easily create custom Message components with the `connectToMessageContainer()` helper
138 |
139 | #### `connectToMessageContainer(componentClass, options: object) -> MessageListener`
140 |
141 | A higher order component that wraps the passed in `componentClass` and injects
142 | container statue as props:
143 |
144 | ```
145 | Options {
146 | methods: array, // methods to passthrough
147 | resolveNames: (
148 | props:object,
149 | container: messageContainerContext
150 | ) -> array,
151 |
152 | mapMessages: (
153 | messages: object,
154 | names: array,
155 | props:object,
156 | container: messageContainerContext
157 | ) -> object,
158 | }
159 | ```
160 |
161 | ### `new Validator(validationFn: (name: string, context: ?any) -> bool)`
162 |
163 | A very simple basic form validator class, to help manage input error state, use is completely optional.
164 | It is designed to nicely hook up to the `MessageContainer` component without being tightly coupled to it.
165 |
166 | #### `validate(names: array, ...context: ?any) -> Promise`
167 |
168 | Returns a promise that resolves with the valid state of the field.
169 | You can validate multiple fields by passing an array. You can also pass in a `context` object which will be passed to the `validationFn`
170 |
171 | #### `validator.isValid(names: array) -> bool`
172 |
173 | Checks if a name is currently in an error state
174 |
175 | #### `errors(names: array) -> object`
176 |
177 | Returns a hash of errors for a set of names;
178 | you can pass this object directly to a `MessageContainer` messages prop
179 |
180 | ```js
181 | let model = { name: '' }
182 |
183 | // you instantiate the object with a function that determines if a field is valid or not
184 | let validator = new Validator(function(fieldName, context){
185 | let isValid = !!context.model[fieldName]
186 |
187 | if (isValid === false)
188 | return [ fieldName + ': is required!']
189 | })
190 |
191 | validator.validate('fieldName', { model: model })
192 | .then(function(isValid){
193 | //do something
194 | })
195 |
196 | validator.isValid('fieldName')
197 | ```
198 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // for jest
2 | module.exports = {
3 | presets: ['@babel/preset-env', '@babel/preset-react'],
4 | }
--------------------------------------------------------------------------------
/dev/input-validator-example.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react')
3 | var Promise = require('promise/lib/es6-extensions')
4 | var Validator = require('../src/Validator')
5 | var MessageContainer = require('../src/MessageContainer.jsx')
6 | var MessageTrigger = require('../src/MessageTrigger.jsx')
7 | var Message = require('../src/Message.jsx')
8 | var connectToMessageContainer = require('../src/connectToMessageContainer')
9 | var RW = require('react-widgets')
10 | var cn = require('classnames');
11 |
12 | require('react-widgets/lib/localizers/globalize')(
13 | require('globalize')
14 | )
15 | /*
16 | * This a simple example showing how you can hook up validation based on specified rules (the traditional way)
17 | * To do this we are going to use `node-validator` as the validation engine.
18 | */
19 | var validator = require('validator')
20 |
21 | // module allows for deep property access via a string path: "foo.bar['baz']"
22 | var getter = require('property-expr').getter
23 | var setter = require('property-expr').setter
24 |
25 | // lets add two custom validators
26 | validator.extend('isPositive', str => parseFloat(str) > 0)
27 | validator.extend('isRequired', str => !!str.length)
28 |
29 | let rules = {
30 | 'personal.name': [
31 | { rule: validator.isRequired, message: 'please enter a name'}
32 | ],
33 | 'personal.birthday': [
34 | { rule: validator.isDate, message: 'please enter a date' }
35 | ],
36 | 'trivia.favNumber': [
37 | { rule: validator.isInt, message: 'please enter an integer' },
38 | { rule: validator.isPositive, message: 'please enter a positive number'}
39 | ]
40 | }
41 |
42 | function FormButton(props) {
43 | let { group, ...childProps } = props;
44 |
45 | return (
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | // Simple component to pull it all together
53 | class App extends React.Component {
54 |
55 | constructor(){
56 | super()
57 |
58 | this._pending = [];
59 |
60 | this.state = {
61 | trivia: {},
62 | personal: {}
63 | }
64 |
65 | // This is the callback used by the validator component
66 | this.validator = new Validator(name => {
67 | var validations = rules[name] || []
68 | , value = getter(name)(this.state)
69 | , error;
70 |
71 | var valid = validations.every(({ rule, message }) => {
72 | var valid = rule(value)
73 |
74 | if (!valid) error = message
75 | return valid
76 | })
77 |
78 | return error
79 | })
80 | }
81 |
82 | _runValidations() {
83 | var pending = this._pending;
84 |
85 | this._pending = []
86 |
87 | if (pending.length)
88 | this.validator.validate(pending)
89 | .then(errors => {
90 | this.setState({ errors })
91 | })
92 | }
93 |
94 | /*
95 | * Lets render a form that validates based on rules specified per input
96 | */
97 | render(){
98 | let model = this.state; // the data to bind to the form
99 |
100 | let handleValidationRequest = event => {
101 | // we will store the paths that need validation and run
102 | // them after the value has been updated if necessary
103 | this._pending = event.fields.concat(this._pending)
104 |
105 | if (event.type !== 'onChange')
106 | this._runValidations();
107 | }
108 |
109 | /*
110 | * This is a little factory method that returns a function that updates the state for a given path
111 | * it creates the `onChange` handlers for our inputs
112 | */
113 | let createHandler = path => {
114 | return val => {
115 | if (val && val.target) // in case we got a `SyntheticEvent` object; react-widgets pass the value directly to onChange
116 | val = val.target.value
117 |
118 | setter(path)(this.state, val)
119 | this.setState(this.state, () => this._runValidations())
120 | }
121 | }
122 |
123 | return (
124 |
125 |
129 |
184 |
185 |
186 | )
187 | }
188 | }
189 |
190 | React.render(, document.body);
191 |
--------------------------------------------------------------------------------
/example/css/styles.css:
--------------------------------------------------------------------------------
1 | .form-control.field-error{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.rv-validation-message{display:none}.rv-validation-message.field-error{color:#a94442;display:inline-block}
--------------------------------------------------------------------------------
/example/dev.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react/addons')
3 | var Validator = require('../src/Validator.jsx')
4 | var FormInput = require('../src/ValidationInput.jsx')
5 | var FormButton = require('../src/ValidateButton.jsx')
6 | var ValidationMessage = require('../src/ValidationMessage.jsx')
7 | var RW = require('react-widgets')
8 | var assign = require('xtend')
9 |
10 | // we'll use yup to define and test our model via a schema
11 | var yup = require('yup')
12 |
13 | // module allows for deep propertay access via a string path: "foo.bar['baz']"
14 | var getter = require('property-expr').getter
15 | var setter = require('property-expr').setter
16 |
17 | /*
18 | * here we define a schema to use as our validator
19 | */
20 | var schema = yup.object({
21 | personal: yup.object({
22 | name: yup.string().required('please provide a name').default(''),
23 | birthday: yup.date().required('please provide a date of birth'),
24 | }),
25 |
26 | trivia: yup.object({
27 |
28 | favNumber: yup.number()
29 | .required().default(0)
30 | .min(0, 'your favorite number cannot be negative')
31 | }),
32 |
33 | }).strict();
34 |
35 | // Simple component to pull it all together
36 | var App = React.createClass({
37 |
38 | getInitialState: function(){
39 | // create a default empty model for the initial value
40 | return schema.default()
41 | },
42 |
43 | /*
44 | * This is a little factory method that returns a function that updates the state for a given path
45 | * it creates the `onChange` handlers for our inputs
46 | */
47 | createHandler(path){
48 | var self = this
49 | , setpath = setter(path)
50 |
51 | return function(val){
52 | var s = self.state; // copy state so we can update without mutating
53 |
54 | if( val && val.target) // in case we got a `SyntheticEvent` object
55 | val = val.target.value
56 |
57 | setpath(s, val === null ? undefined : val) // i don't want to allow nullable values so coerce to undefined
58 | self.setState(s)
59 | }
60 | },
61 |
62 | /*
63 | * This is the callback used by the validator component
64 | */
65 | validate( path, input) {
66 | // The state is updated by widgets, but isn't always synchronous so we check _pendingState first
67 | var state = this._pendingState || this.state
68 |
69 | // `reach()` pulls out a child schema so we can test a single path
70 | var field = yup.reach(schema, path)
71 |
72 | // we also need the specific value for this path
73 | var value = getter(path)(state);
74 |
75 | return field.validate(value, { strict: true })
76 | .then(() => void 0) // if valid return nothing
77 | .catch(err => err.errors) // if invalid return the errors for this field
78 | },
79 |
80 | render(){
81 | var model = this.state; // the data to bind to the form
82 |
83 | return (
84 |
85 |
86 |
131 |
132 |
133 | )
134 | }
135 | })
136 |
137 | React.render(, document.body);
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/example/fonts/rw-widgets.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jquense/react-input-message/789ca91a4dab3575af8aa1b49d11e915fa100bb2/example/fonts/rw-widgets.eot
--------------------------------------------------------------------------------
/example/fonts/rw-widgets.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/example/fonts/rw-widgets.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jquense/react-input-message/789ca91a4dab3575af8aa1b49d11e915fa100bb2/example/fonts/rw-widgets.ttf
--------------------------------------------------------------------------------
/example/fonts/rw-widgets.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jquense/react-input-message/789ca91a4dab3575af8aa1b49d11e915fa100bb2/example/fonts/rw-widgets.woff
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | React Widgets
14 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/example/input-validator-example.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import validator from 'validator'
4 | import V from 'react-input-messages/validator'
5 | import { MessageContainer, MessageTrigger, Message } from 'react-input-messages'
6 |
7 | // module allows for deep property access via a string path: "foo.bar['baz']"
8 | var getter = require('property-expr').getter
9 | var setter = require('property-expr').setter
10 |
11 | // lets add two custom validators
12 | validator.extend('isPositive', str => parseFloat(str) > 0)
13 | validator.extend('isRequired', str => !!str.length)
14 |
15 | let rules = {
16 | 'personal.name': [
17 | { rule: validator.isRequired, message: 'please enter a name'}
18 | ],
19 | 'personal.birthday': [
20 | { rule: validator.isDate, message: 'please enter a date' }
21 | ],
22 | 'trivia.favNumber': [
23 | { rule: validator.isInt, message: 'please enter an integer' },
24 | { rule: validator.isPositive, message: 'please enter a positive number'}
25 | ]
26 | }
27 |
28 | function FormButton(props) {
29 | let { group, ...childProps } = props;
30 |
31 | return (
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | // Simple component to pull it all together
39 | class App extends React.Component {
40 |
41 | constructor(){
42 | super()
43 |
44 | this._pending = [];
45 |
46 | this.state = {
47 | trivia: {},
48 | personal: {}
49 | }
50 |
51 | // This is the callback used by the validator component
52 | this.validator = new V(name => {
53 | var validations = rules[name] || []
54 | , value = getter(name)(this.state)
55 | , error;
56 |
57 | var valid = validations.every(({ rule, message }) => {
58 | var valid = rule(value)
59 |
60 | if (!valid) error = message
61 | return valid
62 | })
63 |
64 | return error
65 | })
66 | }
67 |
68 | _runValidations() {
69 | var pending = this._pending;
70 |
71 | this._pending = []
72 |
73 | if (pending.length)
74 | this.validator.validate(pending)
75 | .then(errors => {
76 | this.setState({ errors })
77 | })
78 | }
79 |
80 | /*
81 | * Lets render a form that validates based on rules specified per input
82 | */
83 | render(){
84 | let model = this.state; // the data to bind to the form
85 |
86 | let handleValidationRequest = event => {
87 | // we will store the paths that need validation and run
88 | // them after the value has been updated if necessary
89 | this._pending = event.fields.concat(this._pending)
90 |
91 | if (event.type !== 'onChange')
92 | this._runValidations();
93 | }
94 |
95 | /*
96 | * This is a little factory method that returns a function that updates the state for a given path
97 | * it creates the `onChange` handlers for our inputs
98 | */
99 | let createHandler = path => {
100 | return val => {
101 | if (val && val.target) // in case we got a `SyntheticEvent` object; react-widgets pass the value directly to onChange
102 | val = val.target.value
103 |
104 | setter(path)(this.state, val)
105 | this.setState(this.state, () => this._runValidations())
106 | }
107 | }
108 |
109 | return (
110 |