Skip to content

Instantly share code, notes, and snippets.

@redhead
Created August 26, 2010 21:16
Show Gist options
  • Save redhead/552273 to your computer and use it in GitHub Desktop.
Save redhead/552273 to your computer and use it in GitHub Desktop.
Live Form Validation for Nette 2.0
/**
* Live Form Validation for Nette 2.0
*
* @author Radek Ježdík, MartyIX, David Grudl
*/
var LiveForm = {
options: {
controlErrorClass: 'form-control-error', // CSS class for an invalid control
errorMessageClass: 'form-error-message', // CSS class for an error message
validMessageClass: 'form-valid-message', // CSS class for a valid message
noLiveValidation: 'no-live-validation', // CSS class for a valid message
showValid: false, // show message when valid
dontShowWhenValidClass: 'dont-show-when-valid', // control with this CSS class will not show valid message
messageTag: 'span', // tag that will hold the error/valid message
messageIdPostfix: '_message', // message element id = control id + this postfix
wait: 300 // delay in ms before validating on keyup/keydown
},
forms: { }
};
/**
* Handlers for all the events that trigger validation
* YOU CAN CHANGE these handlers (ie. to use jQuery events instead)
*/
LiveForm.setUpHandlers = function(el) {
if (this.hasClass(el, this.options.noLiveValidation)) return;
var handler = function(event) {
event = event || window.event;
Nette.validateControl(event.target ? event.target : event.srcElement);
};
var self = this;
el.onchange = handler;
el.onblur = handler;
el.onkeydown = function (event) {
if (self.options.wait >= 200) {
// Hide validation span tag.
self.removeClass(this, self.options.controlErrorClass);
self.removeClass(this, self.options.validMessageClass);
var error = self.getMessageElement(this);
error.innerHTML = '';
error.className = '';
// Cancel timeout to run validation handler
if (self.timeout) {
clearTimeout(self.timeout);
}
}
};
el.onkeyup = function(event) {
event = event || window.event;
if (event.keyCode !== 9) {
if (self.timeout) clearTimeout(self.timeout);
self.timeout = setTimeout(function() {
handler(event);
}, self.options.wait);
}
};
}
LiveForm.addError = function(el, message) {
this.forms[el.form.id].hasError = true;
this.addClass(el, this.options.controlErrorClass);
if (!message) {
message = ' ';
}
var error = this.getMessageElement(el);
error.innerHTML = message;
}
LiveForm.removeError = function(el) {
this.removeClass(el, this.options.controlErrorClass);
var err_el = document.getElementById(el.id + this.options.messageIdPostfix);
if (this.options.showValid && this.showValid(el)) {
err_el = this.getMessageElement(el);
err_el.className = this.options.validMessageClass;
return;
}
if (err_el) {
err_el.parentNode.removeChild(err_el);
}
}
LiveForm.showValid = function(el) {
if(el.type) {
var type = el.type.toLowerCase();
if(type == 'checkbox' || type == 'radio') {
return false;
}
}
var rules = Nette.getRules(null, el);
if(rules.length == 0) {
return false;
}
if (this.hasClass(el, this.options.dontShowWhenValidClass)) {
return false;
}
return true;
}
LiveForm.getMessageElement = function(el) {
var id = el.id + this.options.messageIdPostfix;
var error = document.getElementById(id);
if (!error) {
error = document.createElement(this.options.messageTag);
error.id = id;
el.parentNode.appendChild(error);
}
if (el.style.display == 'none') {
error.style.display = 'none';
}
error.className = this.options.errorMessageClass;
error.innerHTML = '';
return error;
}
LiveForm.addClass = function(el, className) {
if (!el.className) {
el.className = className;
} else if (!this.hasClass(el, className)) {
el.className += ' ' + className;
}
}
LiveForm.hasClass = function(el, className) {
if (el.className)
return el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
return false;
}
LiveForm.removeClass = function(el, className) {
if (this.hasClass(el, className)) {
var reg = new RegExp('(\\s|^)'+ className + '(\\s|$)');
var m = el.className.match(reg);
el.className = el.className.replace(reg, (m[1] == ' ' && m[2] == ' ') ? ' ' : '');
}
}
///////////////////////////////////////////////////////////////////////////////////////////
var Nette = Nette || { };
Nette.addEvent = function (element, on, callback) {
var original = element['on' + on];
element['on' + on] = function () {
if (typeof original === 'function' && original.apply(element, arguments) === false) {
return false;
}
return callback.apply(element, arguments);
};
};
Nette.getValue = function(elem) {
var i, len;
if (!elem) {
return null;
} else if (!elem.nodeName) { // radio
for (i = 0, len = elem.length; i < len; i++) {
if (elem[i].checked) {
return elem[i].value;
}
}
return null;
} else if (elem.nodeName.toLowerCase() === 'select') {
var index = elem.selectedIndex, options = elem.options;
if (index < 0) {
return null;
} else if (elem.type === 'select-one') {
return options[index].value;
}
for (i = 0, values = [], len = options.length; i < len; i++) {
if (options[i].selected) {
values.push(options[i].value);
}
}
return values;
} else if (elem.type === 'checkbox') {
return elem.checked;
} else if (elem.type === 'radio') {
return Nette.getValue(elem.form.elements[elem.name].nodeName ? [elem] : elem.form.elements[elem.name]);
} else {
return elem.value.replace(/^\s+|\s+$/g, '');
}
};
Nette.validateControl = function(elem, rules, onlyCheck) {
rules = Nette.getRules(rules, elem);
for (var id in rules) {
var rule = rules[id], op = rule.op.match(/(~)?([^?]+)/);
rule.neg = op[1];
rule.op = op[2];
rule.condition = !!rule.rules;
var el = rule.control ? elem.form.elements[rule.control] : elem;
var success = Nette.validateRule(el, rule.op, rule.arg);
if (success === null) continue;
if (rule.neg) success = !success;
if (rule.condition && success) {
if (!Nette.validateControl(elem, rule.rules, onlyCheck)) {
return false;
}
} else if (!rule.condition && !success) {
if (el.disabled) continue;
if (!onlyCheck) {
Nette.addError(el, rule.msg.replace('%value', Nette.getValue(el)));
}
return false;
}
}
if (!onlyCheck) {
LiveForm.removeError(elem);
}
return true;
};
Nette.getRules = function(rules, elem) {
return rules || eval('[' + (elem.getAttribute('data-nette-rules') || '') + ']')
};
Nette.validateForm = function(sender) {
var form = sender.form || sender;
LiveForm.forms[form.id].hasError = false;
if (form['nette-submittedBy'] && form['nette-submittedBy'].getAttribute('formnovalidate') !== null) {
return true;
}
var ok = true;
for (var i = 0; i < form.elements.length; i++) {
var elem = form.elements[i];
if (!(elem.nodeName.toLowerCase() in {
input:1,
select:1,
textarea:1
}) || (elem.type in {
hidden:1,
submit:1,
image:1,
reset: 1
}) || elem.disabled || elem.readonly) {
continue;
}
if (!Nette.validateControl(elem)) {
ok = false;
}
}
return ok;
};
Nette.addError = function(elem, message) {
if (elem.focus && !LiveForm.forms[elem.form.id].hasError) {
elem.focus();
}
LiveForm.addError(elem, message);
};
Nette.validateRule = function(elem, op, arg) {
var val = Nette.getValue(elem);
if (elem.getAttribute) {
if (val === elem.getAttribute('data-nette-empty-value')) {
val = '';
}
}
if (op.charAt(0) === ':') {
op = op.substr(1);
}
op = op.replace('::', '_');
op = op.replace(/\\/g,'');
return Nette.validators[op] ? Nette.validators[op](elem, arg, val) : null;
};
Nette.validators = {
filled: function(elem, arg, val) {
return val !== '' && val !== false && val !== null;
},
valid: function(elem, arg, val) {
return Nette.validateControl(elem, null, true);
},
equal: function(elem, arg, val) {
if (arg === undefined) {
return null;
}
arg = Nette.isArray(arg) ? arg : [arg];
for (var i = 0, len = arg.length; i < len; i++) {
if (val == (arg[i].control ? Nette.getValue(elem.form.elements[arg[i].control]) : arg[i])) {
return true;
}
}
return false;
},
minLength: function(elem, arg, val) {
return val.length >= arg;
},
maxLength: function(elem, arg, val) {
return val.length <= arg;
},
length: function(elem, arg, val) {
arg = Nette.isArray(arg) ? arg : [arg, arg];
return (arg[0] === null || val.length >= arg[0]) && (arg[1] === null || val.length <= arg[1]);
},
email: function(elem, arg, val) {
return (/^("([ !\x23-\x5B\x5D-\x7E]*|\\[ -~])+"|[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*)@([0-9a-z\u00C0-\u02FF\u0370-\u1EFF]([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)+[a-z\u00C0-\u02FF\u0370-\u1EFF][-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF]$/i).test(val);
},
url: function(elem, arg, val) {
return (/^(https?:\/\/|(?=.*\.))([0-9a-z\u00C0-\u02FF\u0370-\u1EFF](([-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,61}[0-9a-z\u00C0-\u02FF\u0370-\u1EFF])?\.)*[a-z\u00C0-\u02FF\u0370-\u1EFF][-0-9a-z\u00C0-\u02FF\u0370-\u1EFF]{0,17}[a-z\u00C0-\u02FF\u0370-\u1EFF]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})?(\/\S*)?$/i).test(val);
},
regexp: function(elem, arg, val) {
var parts = typeof arg === 'string' ? arg.match(/^\/(.*)\/([imu]*)$/) : false;
if (parts) {
try {
return (new RegExp(parts[1], parts[2].replace('u', ''))).test(val);
} catch (e) {}
}
},
pattern: function(elem, arg, val) {
try {
return typeof arg === 'string' ? (new RegExp('^(' + arg + ')$')).test(val) : null;
} catch (e) {}
},
integer: function(elem, arg, val) {
return (/^-?[0-9]+$/).test(val);
},
'float': function(elem, arg, val) {
return (/^-?[0-9]*[.,]?[0-9]+$/).test(val);
},
range: function(elem, arg, val) {
return Nette.isArray(arg) ? ((arg[0] === null || parseFloat(val) >= arg[0]) && (arg[1] === null || parseFloat(val) <= arg[1])) : null;
},
submitted: function(elem, arg, val) {
return elem.form['nette-submittedBy'] === elem;
}
};
Nette.toggleForm = function(form) {
for (var i = 0; i < form.elements.length; i++) {
if (form.elements[i].nodeName.toLowerCase() in {
input:1,
select:1,
textarea:1,
button:1
}) {
Nette.toggleControl(form.elements[i]);
}
}
};
Nette.toggleControl = function(elem, rules, firsttime) {
rules = Nette.getRules(rules, elem);
var has = false, __hasProp = Object.prototype.hasOwnProperty, handler = function() {
Nette.toggleForm(elem.form);
};
for (var id in rules) {
var rule = rules[id], op = rule.op.match(/(~)?([^?]+)/);
rule.neg = op[1];
rule.op = op[2];
rule.condition = !!rule.rules;
if (!rule.condition) continue;
var el = rule.control ? elem.form.elements[rule.control] : elem;
var success = Nette.validateRule(el, rule.op, rule.arg);
if (success === null) continue;
if (rule.neg) success = !success;
if (Nette.toggleControl(elem, rule.rules, firsttime) || rule.toggle) {
has = true;
if (firsttime) {
if (!el.nodeName) { // radio
for (var i = 0; i < el.length; i++) {
Nette.addEvent(el[i], 'click', handler);
}
} else if (el.nodeName.toLowerCase() === 'select') {
Nette.addEvent(el, 'change', handler);
} else {
Nette.addEvent(el, 'click', handler);
}
}
for (var id2 in rule.toggle || []) {
if (__hasProp.call(rule.toggle, id2)) {
Nette.toggle(id2, success ? rule.toggle[id2] : !rule.toggle[id2]);
}
}
}
}
return has;
};
Nette.toggle = function(id, visible) {
var elem = document.getElementById(id);
if (elem) {
elem.style.display = visible ? "" : "none";
}
};
Nette.initForm = function(form) {
form.noValidate = 'novalidate';
LiveForm.forms[form.id] = {
hasError: false
};
Nette.addEvent(form, 'submit', function() {
return Nette.validateForm(form);
});
Nette.addEvent(form, 'click', function(e) {
e = e || event;
var target = e.target || e.srcElement;
form['nette-submittedBy'] = (target.type in {
submit:1,
image:1
}) ? target : null;
});
for (var i = 0; i < form.elements.length; i++) {
Nette.toggleControl(form.elements[i], null, true);
LiveForm.setUpHandlers(form.elements[i]);
}
if (/MSIE/.exec(navigator.userAgent)) {
var labels = {},
wheelHandler = function() {
return false;
},
clickHandler = function() {
document.getElementById(this.htmlFor).focus();
return false;
};
for (i = 0, elms = form.getElementsByTagName('label'); i < elms.length; i++) {
labels[elms[i].htmlFor] = elms[i];
}
for (i = 0, elms = form.getElementsByTagName('select'); i < elms.length; i++) {
Nette.addEvent(elms[i], 'mousewheel', wheelHandler); // prevents accidental change in IE
if (labels[elms[i].htmlId]) {
Nette.addEvent(labels[elms[i].htmlId], 'click', clickHandler); // prevents deselect in IE 5 - 6
}
}
}
};
Nette.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
Nette.addEvent(window, 'load', function () {
for (var i = 0; i < document.forms.length; i++) {
Nette.initForm(document.forms[i]);
}
});
@MartyIX
Copy link

MartyIX commented Oct 7, 2011

:) Jo, alespon v poslednim stable Firefoxu, vic jsem netestoval.

@MartyIX
Copy link

MartyIX commented Oct 9, 2011

@bazo
Copy link

bazo commented Apr 4, 2012

na riadku 45 je chyba, namiesto err_el.parentNode.removeChild(el); by malo byt err_el.parentNode.removeChild(err_el); ak mam checkbox a zobrazenu chybovu hlasku tak po kliknuti nan, mi zmizne checkbox namiesto chyby

@redhead
Copy link
Author

redhead commented Apr 4, 2012

Díky!

@f3l1x
Copy link

f3l1x commented Sep 18, 2012

Nevim jestli je to znamy problem. Ale pri pouziti live validace mi nefunguje toggle. Normalne se skryje, ale uz se nezobrazi. Stejny priklad pouziti pri netteForms.js funguje.

@tmysik
Copy link

tmysik commented Nov 25, 2012

Potvrzuji nefunčnost "toggle".

@MilanPala
Copy link

Nefunkční toggle je dán zřejmě přenastavením onchange metody při validaci. Nejdříve se jako onchange handler pro select nastaví toggle a potom se přepíše na validaci. Podle mě stačí řádek 37 nahradit tímto: Nette.addEvent(el, 'change', handler); Samozřejmě by se mělo nahradit i ostatní nastavování handlerů.

@jakubsedlarik
Copy link

Ahoj, při použití minifikátoru JavaScriptPacker vznikl problém u funkcí, které nebyly zakončeny středníkem. Konkrétně mezi řádky 27-153. Viz můj fork.

@vakvas
Copy link

vakvas commented Jun 26, 2013

Jak se Vám chová validace formuláře v IE 10 ? Kliknutím na submit u se mě form pokaždé refreshne a přidá chyby na začátek formu.. ovšem kliknutím na např. povinný input a následně jinam, vyskočí hláška vedle inputu správně.

Ve FF je to ok.

@tomasduracka
Copy link

Přidám řešení problémů, na které jsem narazil:

  • V IE8 funkce getAttribute pro neexistující atribut vrací prázdný řetězec a ne null, takže na řádku 253 je potřeba upravit podmínku, jinak se formulář odešle bez validace (nette/nette@30ac03a).
  • IE < 9 špatně zachází s events ve funkci setTimeout, takže dochází k chybě member not found v handleru onkeyup.
//za řádek 58 (http://stackoverflow.com/questions/3531751/member-not-found-ie-error-ie-6-7-8-9)
var eventCopy = {};
for (var i in event) {
    eventCopy[i] = event[i];
}

//vyměnit původní self.timeout za tento (event je změněn za eventCopy)
self.timeout = setTimeout(function() {
    handler(eventCopy);
}, self.options.wait);
  • Pokud používáte k vykreslování formulářů bootstrap-renderer, tak se hodí využít označení celého chybného ,,řádku" s labelem, inputem a chybovou zprávou. Lze toho docílit přidáním:
//za řádek 75 (funkce LiveForm.addError)
if (document.getElementById(el.id + '-pair') !== null)
        this.addClass(document.getElementById(el.id + '-pair'), 'error');
//za řádek 90 (funkce LiveForm.removeError)
if (document.getElementById(el.id + '-pair') !== null)
        this.removeClass(document.getElementById(el.id + '-pair'), 'error');
//řádek 120 - obecnější vkládání chybové zprávy (řeší problém se skrytou zprávou u datepickeru)
//el.parentNode.appendChild(error);
var blockControl = document.getElementById(el.id + '-pair');
if (blockControl) {
    for (var i = 0; i < blockControl.childNodes.length; i++) {
        if (blockControl.childNodes[i].className === "controls") {
            blockControl.childNodes[i].appendChild(error);
            break;
        }
    }
}

@JanMikes
Copy link

Paradni pluginek, akorat zlobi pri pouziti twitter bootstrapu pri pouziti .input-prepend a .input-append. udelal jsem jednoduchy fix

Stary radek 120:

el.parentNode.appendChild(error);

Nahradit za toto:

if (el.parentNode.className.indexOf("input-prepend") != -1 || el.parentNode.className.indexOf("input-append") != -1) {
    el.parentNode.parentNode.appendChild(error);
} else {
    el.parentNode.appendChild(error);
}

Je to spise workaround nez realny fix, kazdopadne pomohl. Budu rad za pripadnou dalsi upravu ;). Jinak diky, fakt dobra prace.

@JanMikes
Copy link

Tak jsem objevil dalsi chybku. S live validation nefunguje toggle. Staci opravit v metode LiveForm.setUpHandlers zpusob, jakymi se ukladaji handlery na radcich 37, 38, 39 a 55 tak, aby se vyuzivalo Nette.addEvent,
Prepsat zminene radky v tomto stylu:
el.onchange = handler;
prepsat na
Nette.addEvent(el, "change", handler);

Toggly pak zacnou fungovat, duvod je, ze se prepisuje toggle event, misto toho aby se pridaval.

@springpeace
Copy link

Na řádcích 215 a 402 je chyba. Namísto "rules[id]" tam musí být "rules[id][0]". Tedy při použití s Nette > 2.2, nižší verze jsem nezkoušel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment