diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/forms.iml b/.idea/forms.iml new file mode 100644 index 0000000..6b8184f --- /dev/null +++ b/.idea/forms.iml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d16ceba --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0443700 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c80f219 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/LICENSE b/LICENSE index 485b34a..b7f9d50 100644 --- a/LICENSE +++ b/LICENSE @@ -17,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index 19739a5..db2ba65 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,6 @@ For integrating with Twitter bootstrap 3 (horizontal form), this is what you nee }); var bootstrapField = function (name, object) { - object.widget.classes = object.widget.classes || []; object.widget.classes.push('form-control'); @@ -173,7 +172,7 @@ components following the same API. * multipleSelect * label -### Validators +### Field validators * matchField * matchValue @@ -194,6 +193,10 @@ components following the same API. * digits * integer +### Form validators + +* async + ### Renderers * div @@ -217,6 +220,41 @@ Forms can be created with an optional "options" object as well. * `validatePastFirstError`: `true`, otherwise assumes `false` * If `false`, the first validation error will halt form validation. * If `true`, all fields will be validated. +* `validators`: form validators. An example: + + ``` + var forms = require('forms'), + fields = forms.fields, + validators = forms.validators; + + var options = { + validators = [ + validators.async(function(data, next) { + + // eg. post request. + + var err = null; + var response = { + username: { + error: 'User with that name already exists.', + value: 'qwe' + }, + password: { + error: 'Password is to short.', + value: 123 + } + }; + + next(err, response); + }) + ] + }; + + var reg_form = forms.create({ + username: fields.string(), + password: fields.password() + }, options); + ``` ### Form object @@ -347,7 +385,7 @@ Returns a string containing a HTML representation of the widget for the given field. -### Validator +### Field Validator A function that accepts a bound form, bound field and a callback as arguments. It should apply a test to the field to assert its validity. Once processing @@ -369,4 +407,3 @@ containing a HTML representation of the field. [7]: https://nodei.co/npm/forms.png?downloads=true&stars=true [8]: https://npmjs.org/package/forms [9]: http://vb.teelaun.ch/caolan/forms.svg - diff --git a/index.js b/index.js index b7ab242..535c6b9 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ // This file is just added for convenience so this repository can be // directly checked out into a project's deps folder module.exports = require('./lib/forms'); - diff --git a/lib/fields.js b/lib/fields.js index 49e1fba..7b3e4a9 100644 --- a/lib/fields.js +++ b/lib/fields.js @@ -187,4 +187,3 @@ exports.date = function (opt) { exports.object = function (fields, opts) { return forms.create(fields || {}); }; - diff --git a/lib/forms.js b/lib/forms.js index 0d12d71..058ee1e 100644 --- a/lib/forms.js +++ b/lib/forms.js @@ -32,6 +32,8 @@ exports.create = function (fields, opts) { var b = {}; b.toHTML = f.toHTML; b.fields = {}; + b.validators = f.validators || []; + Object.keys(f.fields).forEach(function (k) { if (data != null) { b.fields[k] = f.fields[k].bind(data[k]); @@ -53,7 +55,17 @@ exports.create = function (fields, opts) { callback(validatePastFirstError ? null : err); }); }, function (err) { - callback(err, b); + if (err) { + return callback(err, b); + } + + async.forEach(b.validators, function (validator, callback) { + validator(b, function (err) { + callback(validatePastFirstError ? null : err); + }); + }, function (err) { + callback(err, b); + }); }); }; b.isValid = function () { @@ -111,8 +123,8 @@ exports.create = function (fields, opts) { var kname = is.string(name) ? name + '[' + k + ']' : k; return html + form.fields[k].toHTML(kname, iterator); }, ''); - } + }, + validators: opts.validators }; return f; }; - diff --git a/lib/htmlEscape.js b/lib/htmlEscape.js index cff3246..d21fa11 100644 --- a/lib/htmlEscape.js +++ b/lib/htmlEscape.js @@ -28,4 +28,3 @@ module.exports = (function () { return htmlEscape; }()); - diff --git a/lib/render.js b/lib/render.js index a2d67cc..62b70c2 100644 --- a/lib/render.js +++ b/lib/render.js @@ -52,4 +52,3 @@ exports.table = function (name, field, opt) { return tag('tr', { classes: field.classes() }, th + td); }; - diff --git a/lib/tag.js b/lib/tag.js index 73d0a3a..76be98c 100644 --- a/lib/tag.js +++ b/lib/tag.js @@ -59,4 +59,3 @@ var tag = function tag(tagName, attrsMap, content) { tag.attrs = attrs; module.exports = tag; - diff --git a/lib/validators.js b/lib/validators.js index dceec16..6630694 100644 --- a/lib/validators.js +++ b/lib/validators.js @@ -61,6 +61,18 @@ exports.required = function (message) { }; }; +exports.async = function (asyncFunction) { + return function (form, callback) { + asyncFunction(form.data, function (err, response) { + Object.keys(form.fields).forEach(function (fieldName) { + form.fields[fieldName].error = response[fieldName].error; + form.fields[fieldName].value = response[fieldName].value; + }); + callback(err, form); + }); + }; +}; + exports.requiresFieldIfEmpty = function (alternate_field, message) { if (!message) { message = 'One of %s or %s is required.'; } var validator = function (form, field, callback) { @@ -210,4 +222,3 @@ exports.digits = function (message) { exports.integer = function (message) { return exports.regexp(/^-?\d+$/, message || 'Please enter an integer value.'); }; - diff --git a/lib/widgets.js b/lib/widgets.js index 0206e78..f89659f 100644 --- a/lib/widgets.js +++ b/lib/widgets.js @@ -246,4 +246,3 @@ exports.multipleSelect = function (opt) { }; return w; }; - diff --git a/test-browser.js b/test-browser.js index 415d61c..b1d3a42 100755 --- a/test-browser.js +++ b/test-browser.js @@ -8,4 +8,3 @@ // require('./test/test-validators'); // disabled until this unicode fix is merged: https://github.com/substack/testling/pull/35 require('./test/test-widgets'); }()); - diff --git a/test/test-form.js b/test/test-form.js index 8f9cb39..a77c1df 100644 --- a/test/test-form.js +++ b/test/test-form.js @@ -151,6 +151,90 @@ test('validate invalid data', function (t) { }); }); +test('validate valid data with form validator', function (t) { + t.plan(2); + var fields = { + field1: forms.fields.string(), + field2: forms.fields.string() + }; + + var options = { + validators: [ + function (form, next) { + setTimeout(function () { + form.fields.field1.value = 'field1 value'; + form.fields.field1.error = null; + form.fields.field2.value = 'field2 value'; + form.fields.field2.error = null; + + next(null, form); + }, 100); + } + ] + }; + + var f = forms.create(fields, options); + + f.bind({field1: '1', field2: '2'}).validate(function (err, f) { + t.equal(f.isValid(), true); + t.equal( + f.toHTML(), + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + ); + t.end(); + }); +}); + +test('validate invalid data with form validator', function (t) { + t.plan(2); + var fields = { + field1: forms.fields.string(), + field2: forms.fields.string() + }; + + var options = { + validators: [ + function (form, next) { + setTimeout(function () { + form.fields.field1.value = 'wrongInput1'; + form.fields.field1.error = 'validation error 1'; + form.fields.field2.value = 'wrongInput2'; + form.fields.field2.error = 'validation error 2'; + + next(null, form); + }, 100); + } + ] + }; + + var f = forms.create(fields, options); + + f.bind({field1: '1', field2: '2'}).validate(function (err, f) { + t.equal(f.isValid(), false); + t.equal( + f.toHTML(), + '
' + + '

validation error 1

' + + '' + + '' + + '
' + + '
' + + '

validation error 2

' + + '' + + '' + + '
' + ); + t.end(); + }); +}); + test('handle empty', function (t) { t.plan(3); var f = forms.create({field1: forms.fields.string()}); @@ -444,4 +528,3 @@ test('div bound error', function (t) { t.end(); }); }); - diff --git a/test/test-validators.js b/test/test-validators.js index d6429a5..67560d6 100644 --- a/test/test-validators.js +++ b/test/test-validators.js @@ -427,3 +427,42 @@ test('digits', function (t) { t.end(); }); +test('async', function (t) { + t.test('validator should process response to form object properly', function (st) { + var data = { + fields: { + field1: {data: 'invalid value'}, + field2: {data: 'valid value'} + } + }; + + var asyncFunction = function (data, next) { + var err = null; + var response = { + field1: { + error: 'validation message', + value: 'invalid value' + }, + field2: { + error: null, + value: 'valid value' + } + }; + + next(err, response); + }; + + var v = validators.async(asyncFunction); + st.plan(4); + v(data, function (err, form) { + st.equal(form.fields.field1.error, 'validation message'); + st.equal(form.fields.field1.value, 'invalid value'); + st.equal(form.fields.field2.error, null); + st.equal(form.fields.field2.value, 'valid value'); + + st.end(); + }); + }); + + t.end(); +});