Unique value validation with Knockout-Validation plugin - javascript

I'm using KnockoutJS with the Knockout-Validation plugin to validate fields on a form. I'm having problems validating that a value is unique using the native validation rule - unique
I'm using the Editor Pattern from Ryan Niemeyer to allow the user to edit or create a Location. Here's my fiddle to see my problem in its entirety.
function Location(data, names) {
var self = this;
self.id = data.id;
self.name = ko.observable().extend({ unique: { collection: names }});
// other properties
self.errors = ko.validation.group(self);
// update method left out for brevity
}
function ViewModel() {
var self = this;
self.locations = ko.observableArray([]);
self.selectedLocation = ko.observable();
self.selectedLocationForEditing = ko.observable();
self.names = ko.computed(function(){
return ko.utils.arrayMap(self.locations(), function(item) {
return item.name();
});
});
self.edit = function(item) {
self.selectedLocation(item);
self.selectedLocationForEditing(new Location(ko.toJS(item), self.types));
};
self.cancel = function() {
self.selectedLocation(null);
self.selectedLocationForEditing(null);
};
self.update = function(item) {
var selected = self.selectedLocation(),
updated = ko.toJS(self.selectedLocationForEditing()); //get a clean copy
if(item.errors().length == 0) {
selected.update(updated);
self.cancel();
}
else
alert("Error");
};
self.locations(ko.utils.arrayMap(seedData, function(item) {
return new Location(item, self.types, self.names());
}));
}
I'm having an issue though. Since the Location being edited is "detached" from the locations observableArray (see Location.edit method), when I make changes to name in the detached Location that value isn't updated in the names computed array. So when the validation rule compares it to the names array it will always return a valid state of true since the counter will only ever be 1 or 0. (Please see knockout-validation algorithm below)
Within the options argument for the unique validation rule I can pass in a property for externalValue. If this value is not undefined then it will check to see if the count of matched names is greater or equal to 1 instead of 2. This works except for cases when the user changes the name, goes on to another field, and then goes back to the name and wants to change it back to the original value. The rule just sees that the value already exists in the names array and returns a valid state of false.
Here is the algorithm from knockout.validation.js that handles the unique rule...
function (val, options) {
var c = utils.getValue(options.collection),
external = utils.getValue(options.externalValue),
counter = 0;
if (!val || !c) { return true; }
ko.utils.arrayFilter(ko.utils.unwrapObservable(c), function (item) {
if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) { counter++; }
});
// if value is external even 1 same value in collection means the value is not unique
return counter < (external !== undefined && val !== external ? 1 : 2);
}
I've thought about using this as a base to create a custom validation rule but I keep getting stuck on how to handle the situation when the user wants go back to the original value.
I appreciate any and all help.

One possible solution is to not include the name of the currently edit item (of course when creating a new item you need the full list) in the unique validator.
So the unique check won't be triggered when changing the Location name back to its original value:
self.namesExceptCurrent = function(name){
return ko.utils.arrayMap(self.locations(), function(item) {
if (item.name() !== name)
return item.name();
});
}
self.edit = function(item) {
self.selectedLocation(item);
self.selectedLocationForEditing(
new Location(ko.toJS(item),
self.types,
self.namesExceptCurrent(item.name())));
};
Demo JSFiddle.

Related

Ensure Observable Array is populated before Filter runs

Problem: When My filter method is executing that is used to display an Html element, my observable array has not been populated yet by my web-api call resulting in nothing to filter from.
My Solution :
If I place an alert right before my filter method execution, everything seems to be working.
Length of my observable array is 0 right before my alert call whereas it has a value after my alert.
Question: How can I ensure my array is populated without placing an alert.
This issue is occurring on multiple Pages, placing an alert before my Html is rendered makes everything works fine.
I have a simple Observable Array and a Filter Method on it.
Part of my Knockout Code :
self.currentVendorSupport = ko.observable(new VendorContact());
//Populates Observable Array - allManufactures
self.allManufacturers = ko.observableArray([]);
$.getJSON(serviceRoot + '/api/Manufacturer', function (data) {
var mappedManufacturers = $.map(data, function (item) {
return new Manufacturer(manID = item.manID, name = item.name);
});
self.allManufacturers(mappedManufacturers);
});
//Filters allManufacturers
self.GetCurrentVendor = function () {
alert('allManufacturerLength value again:' + allManufacturerLength);
return ko.utils.arrayFirst(self.allManufacturers(), function (item) {
return item.manID === self.currentVendorSupport().manID();
});
}
It seems to be working.
It is not working with arrayFilter though, is it because of return type difference between the two, wrong syntax or something else?
self.GetCurrentManufacturer = ko.computed(function () {
if (self.allManufacturers().length > 0)
{
return ko.utils.arrayFilter(self.allManufacturers(), function (item)
{
return item.manufacturerID ===
self.currentVendorSupport().manufacturerID() });
}
else return new Manufacturer(0, '...');
}, self);
Html Code:
<label class="control-label readOnlyLabel" data-bind="text: GetCurrentVendor().name"></label>
You can simply make GetCurrentVendor a computedObservable instead so that you can conditionally show a value based on the observable array length. Since it is computed it would react on changes made to the array and update its value.
You can even make it pureComputed so it is only ever activated/computed when called.
For example the computedObservable currentVendor would show "..." when the array is empty and the filtered name when the array is populated.
Computed:
self.currentVendor = ko.computed(function () {
if(this.allManufacturers().length > 0) {
return ko.utils.arrayFirst(this.allManufacturers(), function (item) {
return item.manID === this.currentVendorSupport().manID();
}).name;
} else {
return '...'
}
}, this)
HTML:
<label class="control-label readOnlyLabel" data-bind="text: currentVendor"></label>
Right now, your code is written such that GetCurrentVendor is called only once, by the HTML. And obviously it's called too soon and doesn't get updated afterwards. This is exactly what an observable is for, so that the HTML gets updated when the JS values get updated. So try this:
JS
self.currentVendorSupport = ko.observable(new VendorContact());
//Populates Observable Array - allManufactures
self.allManufacturers = ko.observableArray([]);
//New observable, initially empty
self.currentVendorName = ko.observable();
$.getJSON(serviceRoot + '/api/Manufacturer', function (data) {
var mappedManufacturers = $.map(data, function (item) {
return new Manufacturer(manID = item.manID, name = item.name);
});
self.allManufacturers(mappedManufacturers);
//updated value after api call is complete
self.currentVendorName(self.GetCurrentVendor().name);
});
//Filters allManufacturers
self.GetCurrentVendor = function () {
//alert('allManufacturerLength value again:' + allManufacturerLength);
return ko.utils.arrayFirst(self.allManufacturers(), function (item) {
return item.manID === self.currentVendorSupport().manID();
});
}
HTML
//this automatically updates when a new value is available
<label class="control-label readOnlyLabel" data-bind="text: currentVendorName"></label>

Angular call service function from within service function, reference to this not working

I'm trying to call a service function within another function of the same service, but seeing some strange behavior. Hopefully this is some obvious mistake I am overlooking. Here's the relevant part of my service:
app.factory('Data', ['$http', function($http) {
var Data = this;
var theProduct = {};
var seletedSku = {};
var uniqueItem = {};
return {
product: function(){
return theProduct;
},
getProduct: function(ext_id){
console.log(Data.product);
console.log(this.product);
},
}
}]);
As you can see, within the getProduct() function, I am simply trying to log the product function just to confirm the reference is working correctly. When getProduct() is called, the first line logs undefined, and the second line logs what I would expect, the product function:
function (){
return theProduct;
}
Why is my reference not working? You can see towards the top of the service I save this to the Data variable. Any ideas?
I'm pasting the full service code below just for reference in case it helps:
app.factory('Data', ['$http', function($http) {
var Data = this;
var theProduct = {};
var seletedSku = {};
var uniqueItem = {};
return {
//return the current product being used in the app
product: function(){
return theProduct;
},
//get a products data from the server and set it as the current product being used by the app
getProduct: function(ext_id){
console.log(Data.product);
console.log(this.product);
return $http.post('get_product', {product_id: ext_id}).success(function(data){
theProduct = data;
//when a product is fetched, set the app's unique item
if(theProduct.unique_item){
Data.setUniqueItem(theProduct.unique_item);
}
else{
Data.setUniqueItem({});
}
});
},
//change the currently selected sku for the app
setSku: function(sku){
if(sku){
selectedSku = sku;
}
else{
//null was passed, meaning, the -- Selected SKU -- option
//was chosen, so reset selectedSku back to an empty object
selectedSku = {};
}
//when sku is set, also need to set current unique item
if(selectedSku.unique_item){
Data.setUniqueItem(selectedSku.unique_item);
}
else{
Data.setUniqueItem({});
}
return selectedSku;
},
//get the currently selected sku for the app
sku: function(){
return selectedSku;
},
//set the current unique item
setUniqueItem: function(item){
//before set a unique item, we need to check if the unique
//item we are setting is the same as the current unique item.
//if it is, we need to set them equal so the new item reflects
//current state of it since it's not repulling the item from the database
if(item.id != uniqueItem.id){
//only change the unique item if they are not the same
//if they are the same, just leave unique item as is
uniqueItem = item;
}
console.log(uniqueItem);
return uniqueItem;
},
//get the current unque item
uniqueItem: function(){
return uniqueItem;
}
}
}]);
Because at the time of the reference, this has no context to itself as an object literal.

Initialising a view on keyup to many views - backbone

I have some search functionality that I am working on, every time a user types into a text input I filter a collection, here is the code,
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
resultsList.render();
},
filterUsers: function( collection, filterValue) {
var filteredCollection;
if (filterValue === "") {
return collection.toJSON();
}
return filteredCollection = collection.filter(function(data) {
return _.some(_.values(data.toJSON()), function(value) {
if( value != undefined ) {
value = (!isNaN(value) ? value.toString() : value);
//var re = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return value.indexOf(filterValue) >= 0;
}
});
});
}
As you can see from the code above, I pass a collection (users) and the search parameters to filterUsers(), that then returns a collection of matching models. I am then trying to render that into a list of search results ( links ), but the events on those links run several times (dependent on the length of the search string).
How can I build a list of results from the return collection? I have tried adding,
this.filteredCollection.on('reset', this.doSomething); however this never seems to get run, I have tried initialising my results view in the initialise function also, but I cannot pass the collection to that view as it is empty what is the best way to go?
you have to be careful with views in backbone. You keep adding a new searchresults view without removing the old one. Always keep a reference to views you add multiple times so that you can remove the previous one. I think this part will help you out:
var myCurrentSearchList = null;
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
if (myCurrentSearchList) {
myCurrentSearchList.remove();
}
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
myCurrentSearchList = resultsList;
resultsList.render();
},
http://backbonejs.org/#View-remove

MVC4 ModelState property name to JSON or Array

I am using a complex object graph serialized to JSON with MVC4/jQuery/Sammy/Rivets for SPA functionality.
I have a object graph that looks a bit like this when serialized to JSON (obviously mocked-up):
model =
{
Name: "Me",
Age: 22,
Hobbies:
[
{ Name: "Biking", IsActive: true },
{ Name: "Programming", IsActive: true }
]
}
Everything works quite well until I need Unobtrusive validation, since my Hobbies are in a SlickGrid and I am managing all the data myself. To handle this I am returning my ModelState with my JSON next to my model.
return JSON(new { model = model, modelState = this.ModelState });
From there I intend to iterate through the modelState and assign errors to the right place with some custom function, but there is one problem.
ModelState looks like this:
"Name",
"Age",
"Hobbies[0].Name",
"Hobbies[0].IsActive",
"Hobbies[1].Name",
"Hobbies[1].IsActive"
I need to separate the [0]'s into an object and [1]'s into their own objects so I can smoothly get the values. This gets confusing for me when I begin to account for a third level of complex object array.
Solution:
var ModelStateConverter = function ($, module) {
module = module || {};
// Convert The ModelState form style object to a standard JS object structure.
module.toObject = function (modelState) {
var ModelState = {};
$.each(modelState, function (key, value) {
AssignValuesToObjectStore(key, ModelState, value);
});
return ModelState;
}
// item is the full identifier ex. "Hobbies[0].Name"
// store is the object we are going to throw arrays, objects, and values into.
// value is the error message we want to get in the right place.
// index is an internal processing parameter for arrays only, setting it's value has no effect.
function AssignValuesToObjectStore(item, store, value, index) {
var periodMatch = item.match(/[\.]/);
if (periodMatch === null) {
if (Array.isArray(store)) {
if (store[index] === undefined) {
store[index] = {};
}
store[index][item] = value;
}
else {
store[item] = value;
}
}
else {
// This wasn't a simple property or end of chain.
var currentProperty = item.slice(0, periodMatch.index); // Get our property name up to the first period.
var container = {}; // We assume we are dealing with an object unless proven to be an array.
var arrayIndex; // This is irrelevant unless we have an array.
if (currentProperty.slice(-1, currentProperty.length) === "]") {
// We are dealing with an array! Hoo Ray?!
arrayIndex = parseInt(currentProperty.slice(currentProperty.indexOf("[") + 1, currentProperty.indexOf("]")));
currentProperty = currentProperty.slice(0, currentProperty.indexOf("[")); // remove the indexer ex. [0] so we are left with the real name
container = []; // We know we need an array instead;
}
if (store[currentProperty] === undefined) {
store[currentProperty] = container; // If this property isn't already created, then do so now.
}
//Recurseive nature here.
AssignValuesToObjectStore(item.slice(periodMatch.index + 1, item.length), store[currentProperty], value, arrayIndex);
}
}
return module;
}($, ModelStateConverter);
You can call this from:
ModelStateConverter.toObject(data.modelState);
Where data.modelState is assumed to be the ModelState from the server.
You could try a library like JSON.NET, or the class JavaScriptSerializer, to serialize the ModelState.

Is it possible to log a KO observable array?

I am new to knockoutjs and I have an uber-basic question for you:
I have been able to successfully subscribe to a user changing the on screen twitter handle AND successfully fetch the tweets and display the last recent tweet of a user using console.log(json.results[0].text); However I am not confident that my observable array is working, when I push the json.results into recent tweets: recent_tweets.push(json.results[0].text) I see an [] empty array.
What is going on? Is logging ko.observable array possible?
console.log("TwitterFeedComponent loaded")
TwitterFeedComponent = function(attributes) {
if (arguments[0] === inheriting)
return;
console.log("TwitterFeedComponent() loaded")
var component = this;
var url = 'https://twitter.com/search.json?callback=?';
this.attributes.twitter_user_handle.subscribe(function(value) {
alert("the new value of the twitter handle is " + value);
console.log("I have loaded")
var url = 'https://twitter.com/search.json?callback=?';
var twitter_parameters = {
include_entities: true,
include_rts: true,
q: 'from:' + value,
count: '3'
}
$.getJSON(url,twitter_parameters,
function(json) {
result = json.results[0].text
recent_tweets.push(json.results[0].text);
console.log(recent_tweets);
console.log(json.results[0].text);
});
});
};
To access the actual values of an observable whether it's an array or not you need include parenthesis. For example the following will work:
var recent_tweets= ko.observableArray(["hello", "hi", "how are you"]);
console.log(recent_tweets());
The same is true when assigning variables.
Here is an example of a regular scalar value:
var myObservableName = ko.observable("Luis");
myObservableName("Dany"); // changes the name to: Dany
var Name = myObservableName(); // gets the value and assigns it to the variable Name (in this case the value is "Dany")
To answer this a little differently, you could always use Knockout's subscribe() functionality. Let's assume you have the following view-model:
App.MyViewModel = function() {
var self = this;
self.TestProperty = ko.observable(null);
}
For demonstrations sake, let's assume this property is bound to a text field, as follows:
<input type="text" id="TestPropertyField" data-bind="textInput: TestProperty" />
Now let's assume that you'd like to log any time this value changes. To do this, simply update your view-model as follows:
App.MyViewModel = function() {
var self = this;
self.TestProperty = ko.observable(null);
self.TestProperty.subscribe(function(newValue){
console.log("The new value is: " + newValue);
});
}

Categories