I'm trying to validate an entry in a list to be unique from all other entries in the list using ko.validation, but I'm having issues with validation running when it shouldn't.
I have an editable list (a ko.observableArray), and each item in that array is a view model with a ko.observable on it:
var vm = function (data) {
var self = this;
self.items = ko.observableArray();
_.each(data.words, function (word) {
self.items.push(new listItemVm({parent: self, word: word.word}));
});
};
var listItemVm = function (data) {
var self = this;
self.parent = data.parent;
self.word = ko.observable(data.word);
};
Then I add some validation to listItemVm.word ko.observable. I want each one to be unique:
var listItemVm = function (data) {
var self = this;
self.parent = data.parent;
self.word = ko.observable(data.word).extend({
validation: {
validator: function (name, params) {
console.log("validating " + name);
// word we are editing must be different from all other words
// uncommenting this next line causes the behaviour
// I would expect because params.parent.items()
// is not called
//return true;
var allWords = params.parent.items();
// exclude current view model we are editing
var otherWordViewModels = _.filter(allWords, function (row) {
return row !== params.currentRow;
});
var otherWords = _.map(otherWordViewModels, function (item) {
return item.word();
});
return !_.contains(otherWords, name);
},
message: 'Must be unique',
params: {
currentRow: self,
parent: self.parent
}
}
});
};
I give it some data, and wrap it in some HTML: http://jsfiddle.net/9kw75/3/
Now, this does work - the validation runs correctly and shows invalid when the values of the two inputs are equal - but have a look in the console on that fiddle. Why does the validation routine run three five times on load, and why do both fields validate when just one value updates?
On page load
Expected: validation runs once for each input field.
Actual: validation runs three times for one input, and twice for the other.
On value update (either input field)
Expected: validation runs for altered input field only
Actual: validation runs for both input fields
It's worth noting that this strange behaviour is only observed after reading params.parent.items() in the validator. If the return is commented out, the behaviour I would expect is observed.
I believe the way this works is that the "validator" function is used in a computed observable. Thus, any observables that are read as it executes are now dependencies for the computed. Since you are reading each item's word observable in this function, each one triggers validation for all of the others.
It makes sense that it works this way, though in the case of your particular application, it doesn't make sense. You could use peek to read the observables while not triggering the dependency detection:
var allWords = params.parent.items.peek();
// ...
var otherWords = _.map(otherWordViewModels, function (item) {
return item.word.peek();
});
Related
I have a function that hides and shows items on my page based on what a factory provides me:
function toggleMenuItems(config) {
// hide all our elements first
$(".js-quickMenuElement").hide();
config.data = config.data || [];
config.data.forEach(function (d) {
if (d === config.viewConfigCatalog.CalendarLink) {
$("#CalendarLink.js-quickMenuElement").show();
}
if (d === config.viewConfigCatalog.ProductCreation) {
$("#ProductCreation.js-quickMenuElement").show();
}
// etc etc etc
});
};
We've been using Jasmine for our javascript unit tests and we're discussing whether we should test this function.
Some say that we don't need to because testing this is coupling the view to the javascript test, but at the same time, if instead of jquery .show and .hide functions those were wrappers, or other functions we would test them.
Following on this what would be the best way to test this?
Making a wrapper function that takes in a string and injects the string name in the jQuery select seems wrong.
Another option we thought of is spying on ($.fn, "show") but that would only let us test if show was called X amount of time and not what was hidden...
Thanks,
You can use jQuery to test the visibility of an element.
$(element).is(":visible");
code taken from a related question
Of course in doing this as you say you're coupling the view with the test. You could move the logic which determines the outcome of this function into a separate function and then test that functions result instead.
** Edit **
Below illustrates what I meant regarding simplification with a KVP list, and you could write a test for the function which gets the value from the KVP.
var config = {
data: [],
viewConfigCatalog: {
CalendarLink: "CalendarLink",
ProductCreation: "ProductCreation",
}
};
var kvp = [{
name: config.viewConfigCatalog.CalendarLink,
value: "#CalendarLink.js-quickMenuElement"
}, {
name: config.viewConfigCatalog.ProductCreation,
value: "#ProductCreation.js-quickMenuElement"
}];
function getSelectorString(name) {
var i = kvp.length;
while (i--) {
var pair = kvp[i];
if (pair.name === name)
return pair.value;
}
return null;
}
function toggleMenuItems(config) {
// hide all our elements first
$(".js-quickMenuElement").hide();
config.data = config.data || [];
config.data.forEach(function(d) {
$(getSelectorString(d)).show();
});
};
document.writeln(getSelectorString(config.viewConfigCatalog.CalendarLink)+'<br/>');
document.writeln(getSelectorString(config.viewConfigCatalog.ProductCreation)+'<br/>');
document.writeln(getSelectorString("hi"));
After each click, I intend to empty object editProductList. My code below is instead creating an additional new object editProductList instead of emptying the original editProductList. How do I ensure I'm emptying editProductList instead of creating a new editProductList after clicking it once more?
After the first clicking on the 'devices' column, then #edit_product_add_btn,
I'm logging:
product name, qty: Remote Tag 6
After the second clicking on the 'devices' column, then #edit_product_add_btn, the previous object remains, and it updates both the original object and new one at the same time
product name, qty: Remote Tag 7
product name, qty: Remote Tag 6
Why is it creating an additional object of the same editProductList instead of emptying the original one?
EditableGrid.prototype.mouseClicked = function(e) {
var editProductList = {};
$.ajax({
//...
success: function(response) {
editProductList = JSON.parse(response);
console.log('editProductList direct from DB: ', editProductList);
//adding products into editProductList
$('#edit_product_add_btn').on('click', function(e) {
var in_editProductList = false;
for (var product in editProductList) {
if (editProductList.hasOwnProperty(product)) {
if (editProductList[product].name === productType) {
//...
console.log('product name, qty: ', editProductList[product].name, editProductList[product].qty);
in_editProductList = true;
break;
}
}
}
if (in_editProductList === false) {
//...
var new_product_obj = { name: productType, qty: qty };
editProductList[Object.size(editProductList)] = new_product_obj;
}
});
});
}
After deconstructing your code example it became clear that you want to maintain a shopping cart.
If the user adds a product that also is already in the cart, it should simply increase the quantity of the existing item.
If the user adds a new product, it should append it to the cart.
In both cases the screen should be updated accordingly.
So there are two tasks here. Maintain a list and update the screen.
For updating the screen it is helpful to use an HTML templating library. This helps code readability and reduces the risk surface (no more manual HTML building from strings = less XSS risk). I used Mustache.js in the following example.
It is also helpful to separate the tasks so function size stays manageable.
Note how I use a custom jQuery event (dataUpdated) to decouple screen updating from list maintenance:
$(function () {
var editProductList = [];
var productListItems = Mustache.parse('{{#.}}<li class="list-group-item"><span class="badge">{{qty}}</span>{{name}}<button type="button" class="close" aria-hidden="true">×</button></li>{{/.}}');
EditableGrid.prototype.mouseClicked = function (e) {
if (this.getColumnName(columnIndex) == 'devices') {
$.post('get_requested_devices.php', {
table: this.name,
request_id: this.getRowId(rowIndex)
})
.done(function (response) {
editProductList = response;
$('#edit_product_list').trigger("dataUpdated");
});
}
};
$('#edit_product_list').on("dataUpdated", function () {
var listItems = Mustache.render(productListItems, editProductList);
$('#edit_product_list').empty().append(listItems);
});
$('#edit_product_add_btn').on('click', function (e) {
var qty = parseInt($('#edit_product_qty').val().trim(), 10);
var name = $('#edit_product_type').text().trim();
var existingProduct;
if (qty > 0) {
existingProduct = $.grep(editProductList, function (product) {
return product.name === name;
});
if (existingProduct) {
existingProduct.qty += qty;
} else {
editProductList.push({
name: name,
qty: qty
});
}
$('#edit_product_list').trigger("dataUpdated");
} else {
alert('Enter a number greater than 0');
}
});
});
Warning The above code contains references to two undefined global variables (columnIndex and rowIndex). I have no idea where they come from, I just carried them over from your code. It is a bad idea to maintain global variables for a number of reasons, the biggest one being that many nasty bugs can be traced back to global variables. Try to replace those references, either by function results or by local variables.
Recommendation This situation is the perfect use case of MVVM libraries like Knockout.js. They are designed to completely take over all UI updating for you, so all you need to do is to maintain the data model of the shopping cart. You might want to consider switching.
It's not so clear what you are trying to do.
You seem to be defining a click handler as a method of EditableGrid. So the user will need to click something for the ajax call to be executed, and then click #edit_product_add_btn to load the results into a variable which is local to the first handler? Presumably you are doing something with editProductList after the ajax call comes back, if not loading it at all is pointless, since you can't access it from anywhere else. Perhaps you want this.editProductList instead, so it is accesible from the rest of the "class"?
Are you sure you don't mean to do something like this?
EditableGrid.prototype.mouseClicked = function(e) {
var self = this;
$.ajax({
//...
success: function(response) {
self.editProductList = JSON.parse(response);
}
});
};
About mutating the current object instead of creating a new one: I don't think you gain much from trying to do this. The old object will be garbage collected as soon as the variable points to the new one. You are creating an object as soon as you do JSON.parse, so there's no escaping that.
If I missundersood your intentions, please clarify your question.
I found this example from Ryan Niemeyer and started to manipulate it into the way I write my own code, but then it stopped working. Can anybody tell me why?
Alternative 1 is my variant
Alternative 2 is based upon Ryans solution and does work (just comment/uncomment the Applybindings).
Why doesn´t Alternative 1 work?
My issue is with the filteredRows:
self.filteredRows = ko.dependentObservable(function() {
//build a quick index from the flags array to avoid looping for each item
var statusIndex = {};
ko.utils.arrayForEach(this.flags(), function(flag) {
statusIndex[flag] = true;
});
//return a filtered list
var result = ko.utils.arrayFilter(this.Textbatches(), function(text) {
//check for a matching genré
return ko.utils.arrayFirst(text.genre(), function(genre) {
return statusIndex[genre];
});
return false;
});
console.log("result", result);
return result;
});
I want to filter my Textbatches on the genre-attribute (string in db and the data collected from the db is a string and not an array/object)
Fiddle here: http://jsfiddle.net/gsey786h/6/
You have various problems, most of them can be simply fixed with checking your browser's JavaScript console and reading the exceptions...
So here is the list what you need to fix:
You have miss-typed Textbatches in the declaration so the correct is self.Textbatches = ko.observableArray();
You have scoping problem in filteredRows with the this. So if you are using self you should stick to it and use that:
this.flags() should be self.flags()
this.Textbatches() should be self.Textbatches()
Your genre property has to be an array if you want to use it in ko.utils.arrayFirst
Finally your Textbatch takes individual parameters but you are calling it with an object, so you need to change it to look like:
Textbatch = function(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.statuses = ko.observableArray(data.status);
self.genre = ko.observableArray(data.genre);
self.createdBy = ko.observable(data.createdBy);
};
or you can of course change the calling places to use individual arguments instead of an object.
Here is a working JSFiddle containing all the above mentioned fixes.
I'm creating a custom combobox which uses jQuery validator.
At first they all are gray except the first (it means Country). When I choose 'Slovenská republika' the second combobox is enabled.
They all are instances of a a custom autocomplete combobox widget.
To enable the validation I use this code (which is called within _create: function(){..})
There you can find $.validator.addClassRules(); and $.validator.addMethod(). I also added the appropriate class so it really does something.
_registerCustomValidator: function(){
var uniqueName = this._getUniqueInstanceNameFromThisID(this.id);
var that = this;
console.log(this.id);//this prints 5 unique ids when the page is being loaded
$.validator.addMethod(uniqueName, function(value,element){
if(!that.options.allowOtherValue){
return that.valid;
}
console.log(that.id);//this always prints the ID of the last combobox StreetName
return true;
}, "Error message.");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
//this.id is my own property that I set in _create
Problem: When I change the value of any instance of combobox, it always prints the ID of the last instance StreetName, but it should belong to the one that has been changed.
I thought it might be because of registering $.validator.addMethod("someName",handler) using such a fixed string, so now I pass a uniqueName, but the problem remains.
Therefore the validation of all instances is based on the property allowOtherValue of the last instance.
I don't understand why it behaves so. Does anyone see what might be the problem?
EDIT:
see my comments in the following code
_registerCustomValidator is a custom function within a widget factory.
//somewhere a global var
var InstanceRegistry = [undefined];
//inside a widget factory
_registerCustomValidator: function(){
var i=0;
while(InstanceRegistry[i] !== undefined) ++i;
InstanceRegistry[i] = this.id;
InstanceRegistry[i+1] = undefined;
var ID = i; //here ID,i,InstanceRegistry are correct
$.validator.addMethod(uniqueName, function(value,element){
//here InstanceRegistry contains different values at different positions, so its correct
console.log("ID=="+ID);//ID is always 5 like keeping only the last assiged value.
var that = InstanceRegistry[ID];
if(!that.options.allowOtherValue){
return that.valid;
}
return true;
}, "Error message");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
It looks like a sneaky combination of closure logic and reference logic. The callback in $.validator.addMethod is enclosing a reference to this which will equal the last value of this when $.validator.addMethod. (Or something like that?)
Glancing at your code, it's not clear to me what this is in this context. So I can't really offer a concrete solution. But one solution might be to create some kind of global registry for your thises. Then you could do something along the lines of:
_registerCustomValidator: function(){
var uniqueName = this._getUniqueInstanceNameFromThisID(this.id);
$.validator.addMethod(uniqueName, function(value,element) {
var instance = InstanceRegistry[uniqueName];
if(! instance.options.allowOtherValue){
return instance.valid;
}
return true;
}, "Error message.");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
The registry could be keyed to uniqueName or id, just so long as it is a value getting enclosed in your callback.
I've got problem with getting knockout validation to work as I want to.
Let say we have ViewModel:
var viewModel = function ViewModel() {
var self = this;
self.number = ko.observable("").extend({ pattern: new RegExp("^(\\d{10})$") });
self.submitFunction = function () {
if (self.number.isValid()) {
//SUBMIT
}
};
}
In this situation when self.number is empty function return true. It's great when we are adding error messages( field is not red from the start). But when I'm submitting value it should be considered invalid and UI validation suppose to be updated.
What is good practice here?