Skip to content

Commit

Permalink
Adding ValidationMixin.validateValue. Adding error msgs on field chan…
Browse files Browse the repository at this point in the history
…ge to runners and readme's.
  • Loading branch information
eddyystop committed Jun 20, 2014
1 parent 314a000 commit 4d2b2a7
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 76 deletions.
18 changes: 16 additions & 2 deletions README.md
Expand Up @@ -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.

Expand All @@ -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.
Expand Down
20 changes: 7 additions & 13 deletions app.js
Expand Up @@ -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' },
Expand All @@ -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' },
Expand Down
50 changes: 34 additions & 16 deletions mixins/FormMixin/readme
Expand Up @@ -38,47 +38,65 @@
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
this.validator.validate(validations);
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', [ // <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 ...'; }
Expand Down
48 changes: 48 additions & 0 deletions 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]).*$';
};
}
23 changes: 17 additions & 6 deletions 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 = [];
}
Expand All @@ -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
Expand Down
61 changes: 42 additions & 19 deletions mixins/ValidationMixin/readme
Expand Up @@ -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 ==========
<script src="js/vendor/mithril.js"></script>
<script src="js/vendor/validator.js"></script>
<script src="../mixins/Solder/Solder.js"></script>
Expand All @@ -34,47 +39,65 @@
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
this.validator.validate(validations);
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', [ // <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 ...'; }
Expand Down

0 comments on commit 4d2b2a7

Please sign in to comment.