From 4d2b2a75910b2e2b6857906a36d9b22f9005702f Mon Sep 17 00:00:00 2001 From: eddyystop Date: Fri, 20 Jun 2014 08:38:33 -0400 Subject: [PATCH] Adding ValidationMixin.validateValue. Adding error msgs on field change to runners and readme's. --- README.md | 18 ++++++- app.js | 20 +++----- mixins/FormMixin/readme | 50 +++++++++++++------ mixins/PatternsMixin/PatternsMixin.js | 48 ++++++++++++++++++ mixins/ValidationMixin/ValidationMixin.js | 23 ++++++--- mixins/ValidationMixin/readme | 61 ++++++++++++++++------- public/js/FormMixin-runner.js | 50 +++++++++++++------ public/js/ValidationMixin-runner.js | 21 ++++++-- public/js/vendor/validator.js | 1 - 9 files changed, 216 insertions(+), 76 deletions(-) create mode 100644 mixins/PatternsMixin/PatternsMixin.js diff --git a/README.md b/README.md index afda065..49af824 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ A working example, if any, is located at ```public/ValidationMixin.html```. - ```mixins/Solder.js``` - A simple dependency injector for mixins. Mixin extension is supported. - ```Solder-extend``` - Extending a mixin with new features. -- ```ValidationMixin``` - A data validator -integrated with [validator.js](https://github.com/chriso/validator.js) +- ```ValidationMixin``` - A data validator, for one field or the entire form. +Can be used to display error messages on (e.g.) onchange. +Integrated with [validator.js](https://github.com/chriso/validator.js). - ```FormMixin``` - Adds support for forms, with or without a < form >. Requires the server. @@ -59,6 +60,19 @@ A working example, if any, is located at ```public/seo-by-cleanup.html``` - ```service-error-handling``` - Handle web-server and app errors +## Services + +-```progress bars``` - How to show a progress bar. + +``` +var Thing = { + list: function(options) { + return m.request({method: "GET", url: "/thing", config: function(xhr) { xhr.onprogress = options.progress }}) + } +} +Thing.list({progress: function(e) { console.log("progress: ", e) }}) +``` + ### SEO / server rendered first page - ```seo-by-noscript``` - Server serves SEO the first page within a noscript tag. diff --git a/app.js b/app.js index c5c636f..3bc1f31 100644 --- a/app.js +++ b/app.js @@ -32,6 +32,8 @@ app.use(express.errorHandler()); app.get('/user', function (req, res) { console.log('.....route /user'); + console.log('body=', req.body); + console.log('query=', req.query); res.send([ { id: 1, name: 'John' }, { id: 2, name: 'Mary' }, @@ -43,23 +45,15 @@ app.get('/user', function (req, res) { app.post('/form', function (req, res) { console.log('.....route POST /form'); - console.log(req.originalUrl); // /form - console.log(req.body); // undefined - console.log(req.query); // {} - res.send([ - { id: 1, name: 'John' }, - { id: 2, name: 'Mary' }, - { id: 3, name: 'Nick' }, - { id: 4, name: 'Stephane' }, - { id: 5, name: 'Jessica' } - ]); + console.log('body=', req.body); + console.log('query=', req.query); + res.send({ appReply: 'okay' }); }); app.get('/form', function (req, res) { console.log('.....route GET /form'); - console.log(req.originalUrl); // /form?name=514.9999999994179 - console.log(req.body); // {} - console.log(req.query); // {} + console.log('body=', req.body); + console.log('query=', req.query); res.json({ data: [ { id: 1, name: 'John' }, diff --git a/mixins/FormMixin/readme b/mixins/FormMixin/readme index 7262b5f..d6b15a7 100644 --- a/mixins/FormMixin/readme +++ b/mixins/FormMixin/readme @@ -38,13 +38,22 @@ var v = this.validator.checks; var validations = { - name: function (name) { return v.isLength(self.name(),4, 10); } + name: function (nameValue) { return v.isLength(nameValue,4, 10); }, // name refers to this.name + dept: function (deptValue) { return v.isLength(deptValue, 1); } }; - this.name = m.prop(window.performance.now() + ''); + this.name = m.prop('John'); + this.dept = m.prop('dev'); + + this.check = function (attr, key) { + return function (e) { + m.withAttr(attr, self[key])(e); + self.validator.validateValue(key, validations); + }; + }; this.submit = function (e) { - e.preventDefault(); // in case its a submit + e.preventDefault(); e.stopPropagation(); // validate @@ -52,33 +61,42 @@ if (!self.validator.hasErrors()) { // post this.form.submitForm( - { method: 'POST', url: '/form', data: { name: this.name() }, contentType: 'application/json; charset=utf-8' }, - success, failure); - } - - function success () { - console.log('success. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); - } - function failure () { - console.log('failure. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + { + method: 'POST', url: '/form', + data: { name: this.name(), dept: this.dept() }, + contentType: 'application/json; charset=utf-8' + }, + function (obj) { log('success', obj); }, + function (str) { log('failure', str); } + ); } }.bind(this); + + function log (str, load) { + console.log(str + '. form.state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + console.log('load=', load); + } }, view: function (ctrl) { return m('form', [ //
is optional - m('div', 'name (4+ chars)'), + m('div', 'name (4 to 10 chars)'), m('div', [ m('input' + (ctrl.validator.hasError('name') ? '.error' : ''), - { value: ctrl.name(), onchange: m.withAttr('value', ctrl.name ) }), + { value: ctrl.name(), onchange: ctrl.check('value', 'name') }), ctrl.validator.hasError('name') ? m('p.error.error-msg', 'The name must be 4 to 10 chars.') : '' ]), + m('div', [ + m('input' + (ctrl.validator.hasError('dept') ? '.error' : ''), + { value: ctrl.dept(), onchange: ctrl.check('value', 'dept') }), + ctrl.validator.hasError('dept') ? m('p.error.error-msg', 'A department name is required.') : '' + ]), m('button[type=button]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=button]'), m('button[type=submit]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=submit]'), - msg() + postStatus() ]); - function msg () { + function postStatus () { var status = ''; if (ctrl.form.getError()) { status = 'An error has occurred. Please try again.'; } else if (ctrl.form.isSubmitting()) { status = 'Processing ...'; } diff --git a/mixins/PatternsMixin/PatternsMixin.js b/mixins/PatternsMixin/PatternsMixin.js new file mode 100644 index 0000000..4a0c2d4 --- /dev/null +++ b/mixins/PatternsMixin/PatternsMixin.js @@ -0,0 +1,48 @@ +/* I've decided ValidationMixin is more useful than this. */ +/* Extracted and extended from http://foundation.zurb.com/docs/components/abide.html */ +// http://www.html5rocks.com/en/tutorials/forms/constraintvalidation/ +// browsers do display the contents of the title attribute in the inline bubble if it's provided. + +// Patterns ==================================================================== +function Patterns (ctrl) { + if (!(this instanceof Patterns)) { return new Patterns(ctrl); } + this._ctrl = ctrl; + + // Abide patterns ------------------------------------------------------------ + this.alpha = /^[a-zA-Z]+$/; + this.alpha_numeric = /^[a-zA-Z0-9]+$/; + this.integer = /^[-+]?\d+$/; + this.number = /^[-+]?[1-9]\d*$/; + // amex, visa, diners + this.card = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/; + this.cvv = /^([0-9]){3,4}$/; + // http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address + this.email = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + this.url = /(https?|ftp|file|ssh):\/\/(((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-zA-Z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-zA-Z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-zA-Z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?/; + // abc.de + this.domain = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/; + this.datetime = /([0-2][0-9]{3})\-([0-1][0-9])\-([0-3][0-9])T([0-5][0-9])\:([0-5][0-9])\:([0-5][0-9])(Z|([\-\+]([0-1][0-9])\:00))/; + // YYYY-MM-DD + this.date = /(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))/; + // HH:MM:SS + this.time = /(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}/; + this.dateISO = /\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; + // MM/DD/YYYY + this.month_day_year = /(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.](19|20)\d\d/; + // #FFF or #FFFFFF + this.color = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/; + + // Other patterns ------------------------------------------------------------ + this._min = function (min) { return min !== undefined ? min + '' : '0'; }; + this._max = function (max) { return max !== undefined ? max + '' : ''; }; + this.alphaLen = function (min, max) { + return '^[a-zA-Z]{' + this._min(min) + ',' + this._max(max) + '}$'; + }; + this.alpha_numericLen = function (min, max) { + return '^[a-zA-Z0-9]{' + this._min(min) + ',' + this._max(max) + '}$'; + }; + this.passwordLen = function (min, max) { + return '(?=^.{' + this._min(min) + ',' + this._max(max) + + '}$)((?=.*\\d)|(?=.*\\W+))(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$'; + }; +} \ No newline at end of file diff --git a/mixins/ValidationMixin/ValidationMixin.js b/mixins/ValidationMixin/ValidationMixin.js index 670c02c..c825aa4 100644 --- a/mixins/ValidationMixin/ValidationMixin.js +++ b/mixins/ValidationMixin/ValidationMixin.js @@ -1,6 +1,7 @@ // ValidationMixin ============================================================= function ValidationMixin (ctrl) { if (!(this instanceof ValidationMixin)) { return new ValidationMixin(ctrl); } + this.self = this; this.ctrl = ctrl; this.errors = []; } @@ -11,14 +12,24 @@ ValidationMixin.prototype = { hasError: function (key) { return this.errors.indexOf(key) !== -1; }, validate: function (validations) { - var ctrl = this.ctrl; + var self = this; this.errors = Object.keys(validations).filter(function (key) { - var value = ctrl[key]; - if (typeof value === 'function') { value = value(); } + return !validations[key](self._resolveKey(key)); + }); + return this.errors.length; + }, + + validateValue: function (key, validations) { + var i = this.errors.indexOf(key); + if (i !== -1) { this.errors.splice(i, 1); } + var result = validations[key](this._resolveKey(key)); + if (!result) this.errors.push(key); + return result; + }, - return !validations[key](value); - } - ); + _resolveKey: function (key) { + var value = this.ctrl[key]; + return typeof value === 'function' ? value() : value; }, checks: window.validator || {} // github.com/chriso/validator.js diff --git a/mixins/ValidationMixin/readme b/mixins/ValidationMixin/readme index 3d7ece4..7eb3254 100644 --- a/mixins/ValidationMixin/readme +++ b/mixins/ValidationMixin/readme @@ -4,16 +4,21 @@ * * Injected property contains: * {fcn} validate ({ name1: fcn, name2: fcn, ... }): validate fields. + * {str} name1: data to validate, either this[name1] or this[name1]() if m.prop-like. + * {fcn} fcn (name): function to validate field. Returns {bin} if valid. + * returns {bin} if all values are valid. + * {fcn} validateValue (name, { name1: fcn1, name2: fcn2, ... }) * {str} name: data to validate, either this[name] or this[name]() if m.prop-like. - * {fcn} fcn: function to validate field. + * {str} name1: as in .validate above. + * {fcn} fcn: as in .validate above. + * returns {bin} if value is valid. * {fcn} hasErrors: if any data is invalid. * {fcn} hasError (name): if field 'name' is invalid. * {fcn} clearErrors : clear errors. * * The christo/validator.js methods are exposed via ValidationMixin.checks. * - * SAMPLE USAGE ==================================================================== - + * SAMPLE USAGE: display error messages as fields change, and on submit ========== @@ -34,13 +39,22 @@ var v = this.validator.checks; var validations = { - name: function (name) { return v.isLength(self.name(),4, 10); } + name: function (nameValue) { return v.isLength(nameValue,4, 10); }, // name refers to this.name + dept: function (deptValue) { return v.isLength(deptValue, 1); } }; - this.name = m.prop(window.performance.now() + ''); + this.name = m.prop('John'); + this.dept = m.prop('dev'); + + this.check = function (attr, key) { + return function (e) { + m.withAttr(attr, self[key])(e); + self.validator.validateValue(key, validations); + }; + }; this.submit = function (e) { - e.preventDefault(); // in case its a submit + e.preventDefault(); e.stopPropagation(); // validate @@ -48,33 +62,42 @@ if (!self.validator.hasErrors()) { // post this.form.submitForm( - { method: 'POST', url: '/form', data: { name: this.name() }, contentType: 'application/json; charset=utf-8' }, - success, failure); - } - - function success () { - console.log('success. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); - } - function failure () { - console.log('failure. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + { + method: 'POST', url: '/form', + data: { name: this.name(), dept: this.dept() }, + contentType: 'application/json; charset=utf-8' + }, + function (obj) { log('success', obj); }, + function (str) { log('failure', str); } + ); } }.bind(this); + + function log (str, load) { + console.log(str + '. form.state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + console.log('load=', load); + } }, view: function (ctrl) { return m('form', [ // is optional - m('div', 'name (4+ chars)'), + m('div', 'name (4 to 10 chars)'), m('div', [ m('input' + (ctrl.validator.hasError('name') ? '.error' : ''), - { value: ctrl.name(), onchange: m.withAttr('value', ctrl.name ) }), + { value: ctrl.name(), onchange: ctrl.check('value', 'name') }), ctrl.validator.hasError('name') ? m('p.error.error-msg', 'The name must be 4 to 10 chars.') : '' ]), + m('div', [ + m('input' + (ctrl.validator.hasError('dept') ? '.error' : ''), + { value: ctrl.dept(), onchange: ctrl.check('value', 'dept') }), + ctrl.validator.hasError('dept') ? m('p.error.error-msg', 'A department name is required.') : '' + ]), m('button[type=button]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=button]'), m('button[type=submit]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=submit]'), - msg() + postStatus() ]); - function msg () { + function postStatus () { var status = ''; if (ctrl.form.getError()) { status = 'An error has occurred. Please try again.'; } else if (ctrl.form.isSubmitting()) { status = 'Processing ...'; } diff --git a/public/js/FormMixin-runner.js b/public/js/FormMixin-runner.js index ccf37de..4aff18b 100644 --- a/public/js/FormMixin-runner.js +++ b/public/js/FormMixin-runner.js @@ -14,13 +14,22 @@ var app = { var v = this.validator.checks; var validations = { - name: function (name) { return v.isLength(self.name(),4, 10); } + name: function (nameValue) { return v.isLength(nameValue,4, 10); }, // name refers to this.name + dept: function (deptValue) { return v.isLength(deptValue, 1); } }; - this.name = m.prop(window.performance.now() + ''); + this.name = m.prop('John'); + this.dept = m.prop('dev'); + + this.check = function (attr, key) { + return function (e) { + m.withAttr(attr, self[key])(e); + self.validator.validateValue(key, validations); + }; + }; this.submit = function (e) { - e.preventDefault(); // in case its a submit + e.preventDefault(); e.stopPropagation(); // validate @@ -28,33 +37,42 @@ var app = { if (!self.validator.hasErrors()) { // post this.form.submitForm( - { method: 'POST', url: '/form', data: { name: this.name() }, contentType: 'application/json; charset=utf-8' }, - success, failure); - } - - function success () { - console.log('success. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); - } - function failure () { - console.log('failure. state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + { + method: 'POST', url: '/form', + data: { name: this.name(), dept: this.dept() }, + contentType: 'application/json; charset=utf-8' + }, + function (obj) { log('success', obj); }, + function (str) { log('failure', str); } + ); } }.bind(this); + + function log (str, load) { + console.log(str + '. form.state=', self.form._formState, 'status=', self.form._xhrStatus, 'error=', self.form._formError); + console.log('load=', load); + } }, view: function (ctrl) { return m('form', [ // is optional - m('div', 'name (4+ chars)'), + m('div', 'name (4 to 10 chars)'), m('div', [ m('input' + (ctrl.validator.hasError('name') ? '.error' : ''), - { value: ctrl.name(), onchange: m.withAttr('value', ctrl.name ) }), + { value: ctrl.name(), onchange: ctrl.check('value', 'name') }), ctrl.validator.hasError('name') ? m('p.error.error-msg', 'The name must be 4 to 10 chars.') : '' ]), + m('div', [ + m('input' + (ctrl.validator.hasError('dept') ? '.error' : ''), + { value: ctrl.dept(), onchange: ctrl.check('value', 'dept') }), + ctrl.validator.hasError('dept') ? m('p.error.error-msg', 'A department name is required.') : '' + ]), m('button[type=button]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=button]'), m('button[type=submit]', { onclick: ctrl.submit, disabled: !ctrl.form.isEditable() }, 'Submit [type=submit]'), - msg() + postStatus() ]); - function msg () { + function postStatus () { var status = ''; if (ctrl.form.getError()) { status = 'An error has occurred. Please try again.'; } else if (ctrl.form.isSubmitting()) { status = 'Processing ...'; } diff --git a/public/js/ValidationMixin-runner.js b/public/js/ValidationMixin-runner.js index 6c2a160..5042d3c 100644 --- a/public/js/ValidationMixin-runner.js +++ b/public/js/ValidationMixin-runner.js @@ -8,6 +8,7 @@ solder.setMixin('validator', ValidationMixin); var app = { // model name: m.prop('John'), + dept: m.prop('dev'), // app controller: function () { @@ -17,13 +18,22 @@ var app = { var v = this.validator.checks; var validations = { - name: function (name) { return v.isLength(self.name(),4, 10); }, + name: function (name) { return v.isLength(self.name(),4, 10);}, + dept: function (dept) { return v.isLength(self.dept(), 1); }, foo: function (foo) { return typeof foo !== 'string'; } }; this.name = app.name; // m.prop() thingy + this.dept = app.dept; this.foo = 5; // not a m.prop() thingy + this.check = function (attr, key) { + return function (e) { + m.withAttr(attr, self[key])(e); + self.validator.validateValue(key, validations); + }; + }; + this.submit = function () { this.validator.validate(validations); if (!self.validator.hasErrors()) { app.name = self.name; } @@ -32,12 +42,17 @@ var app = { view: function (ctrl) { return [ - m('div', 'name (4+ chars)'), + m('div', 'name (4 to 10 chars)'), m('div', [ m('input' + (ctrl.validator.hasError('name') ? '.error' : ''), - { value: ctrl.name(), onchange: m.withAttr('value', ctrl.name ) }), + { value: ctrl.name(), onchange: ctrl.check('value', 'name') }), ctrl.validator.hasError('name') ? m('p.error.error-msg', 'The name must be 4 to 10 chars.') : '' ]), + m('div', [ + m('input' + (ctrl.validator.hasError('dept') ? '.error' : ''), + { value: ctrl.dept(), onchange: ctrl.check('value', 'dept') }), + ctrl.validator.hasError('dept') ? m('p.error.error-msg', 'A department name is required.') : '' + ]), m('button', {onclick: ctrl.submit}, 'Submit') ]; } diff --git a/public/js/vendor/validator.js b/public/js/vendor/validator.js index 9f33ace..f7cd69c 100644 --- a/public/js/vendor/validator.js +++ b/public/js/vendor/validator.js @@ -220,7 +220,6 @@ }; validator.isLength = function (str, min, max) { - console.log('.in isLength arguments=', arguments ); var surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g) || []; var len = str.length - surrogatePairs.length; return len >= min && (typeof max === 'undefined' || len <= max);