Knockout validation for empty input - javascript

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?

Related

Knockout: observableArray of arrays of observable inputs

I am having problems with route.html's foreach binding in the project Flight Management Computer.
Problem 1: value bindings in observableArray all update simultaneously
For the route in javascript, I set up a ko.observableArray of an Array of ko.observable (sounds very confusing, but code attached below nonetheless):
/* All bindings applied to viewmodel already */
var route = ko.observableArray();
var DEFAULT_ROUTE = [
ko.observable(), // Waypoint Name
ko.observable(), // Lat.
ko.observable(), // Lon.
ko.observable(), // Altitude Restriction
ko.observable(false), // Waypoint isValid
ko.observable('') // Waypoint information
];
Clicking a specific button adds the DEFAULT_ROUTE with no problem, as it calls
route.push(DEFAULT_ROUTE);
The HTML Code looks roughly like this and have no UI issues:
<tbody data-bind="foreach: route">
<tr class="wpt-row">
<td><input data-bind="value: $data[0]"></td> <!--waypoint input-->
<td><input data-bind="value: $data[1]"></td> <!--lat. input-->
<td><input data-bind="value: $data[2]"></td> <!--lon. input-->
<td><input data-bind="value: $data[3]"></td> <!--alt. input-->
</tr>
</tbody>
However, problems arise when there are multiple arrays in the outer ko.observableArray, as changing one input value both in the UI and in javascript will update ALL values in each array. Example:
var route = ko.observableArray([DEFAULT_ROUTE, DEFAULT_ROUTE, DEFAULT_ROUTE]);
// Then, outside viewmodel (in javascript console)
route()[0][0]('WPT'); // Sets the waypoint of the first input field to be 'WPT'
// Later
route()[0][0](); // 'WPT', correct
route()[1][0](); // 'WPT', incorrect, should be undefined
route()[2][0](); // 'WPT', incorrect, should be undefined
I set up a similar foreach in a different file, but with <input> simply as <span>, and data-bind as text: $data[x] instead of value. That different file works fine with no problems. The different file is log.html
Problem 2 (or rather, a Question)
After the route problem is fixed, I wish to update some specific values in a single array (one waypoint input field) when another value in that same array changes. I.E.
// Scenario 1, waypoint is a valid waypoint with proper coords
var waypoint = 'WAATR';
var coords = getWaypoint(waypoint); // [42.1234, -70.9876]
route()[0][0](waypoint);
// route()[0][0]() is now 'WAATR'
// route()[0][1] and route()[0][2] should automatically update with value `coords[0]` and `coords[1]`
// route()[0][4] should be set to true (valid waypoint)
// Scenario 2, waypoint is NOT a valid waypoint
var waypoint = 'IDK';
var coords = getWaypoint(waypoint); // []
route()[0][0](waypoint);
// route()[0][0]() is now 'IDK'
// route()[0][1] and route()[0][2] should remain undefined, waiting for users to manually input coordinates
// route()[0][4] should be false (invalid waypoint)
I read the documentation and there is an extend function, but I don't really understand it. The challenge right now is how to limit those automatic fill-in functions to a specific array (waypoint input field) instead of (like Problem #1) to the entire data table of input.
I would greatly appreciate if anybody could help, since the route is the most important feature of the entire project.
You should really use objects rather than arrays. It makes everything much easier to read and understand, and will greatly help debugging.
var Route = function() {
this.waypointName = ko.observable();
this.lat = ko.observable();
this.lon = ko.observable();
this.altitudeRestriction = ko.observable();
this.isValid = ko.observable(false);
this.waypointInfo = ko.observable('');
};
Like you already figured out, you can now use this by calling new Route(). You'll solve issue 1 and have code that's easier to read and mantain. The right foundation to solve issue 2:
Because you now have a clearly defined model, you can start defining relations between the properties by using subscribe or computed. You want to change the waypointName property and have other properties automatically update:
var Route = function() {
this.waypointName = ko.observable();
// Automatically updates when you set a new waypoint name
var coords = ko.pureComputed(function() {
return getWaypoint(this.waypointName());
}, this);
// Check if we got correct coords
this.isValid = ko.pureComputed(function() {
return coords().length === 2;
}, this);
// Auto-extract lat from coords, null if invalid
this.lat = ko.pureComputed(function() {
return this.isValid()
? coords()[0]
: null;
}, this);
// Auto-extract lat from coords, null if invalid
this.lon = ko.pureComputed(function() {
return this.isValid()
? coords()[1]
: null;
}, this);
};
You now have a default Route with isValid: false, lat: null, lon:null and when you set the waypointName to a string value, like route.waypointName("WAATR"), all properties will automatically update.
Question 1:
This is rather an javascript related problem, than a knockoutjs. You are push-ing a reference to the same object again and again, and thereby making your observableArray contain multiple references to same object. You should change your code, to use a factory function instead:
var DEFAULT_ROUTE = function(){
return [
ko.observable(), // Waypoint Name
ko.observable(), // Lat.
ko.observable(), // Lon.
ko.observable(), // Altitude Restriction
ko.observable(false), // Waypoint isValid
ko.observable('') // Waypoint information
];
};
And then pushing:
route.push(DEFAULT_ROUTE());
This way you add a new object each time.
Hi user3297291, thank you for your kind help! Based on your suggestion, I was able to complete the function:
var Route = function () {
var self = this;
// Waypoint name
var _fix = ko.observable();
self.fix = ko.pureComputed({
read: function () {
return _fix();
},
write: function (val) {
_fix(val);
var coords = get.waypoint(val);
var isValid = coords[0] && coords[1];
self.lat(coords[0], isValid);
self.lon(coords[1], isValid);
self.info(coords[2]);
}
});
// Latitude
var _lat = ko.observable();
self.lat = ko.pureComputed({
read: function () {
return _lat();
},
write: function (val, isValid) {
_lat(val);
self.valid(isValid ? true : false);
}
});
// longitude
var _lon = ko.observable();
self.lon = ko.pureComputed({
read: function () {
return _lon();
},
write: function (val, isValid) {
_lon(val);
self.valid(isValid ? true : false);
}
});
// Is waypoint valid
self.valid = ko.observable(false);
// Waypoint info
self.info = ko.observable();
};

How to prevent knockout.js from deleting form field values prefilled by FireFox when applying bindings

FireFox prefills certain form values in input boxes after loading pages (such as usernames etc...).
If I apply Knockout.js bindings to a prefilled form, at the moment of applying bindings Knockout will clear out the input fields (causing short flicker).
Is there a way to keep the prefilled values rather than erasing them?
var UserModel = function() {
this.username = ko.observable();
this.password = ko.observable();
this.passwordRepeat = ko.observable();
....
}
....
domReady(function() {
//values prefilled by FireFox in input box bound to username are
//erased after applyBindings is executed
ko.applyBindings(new UserModel());
});
You can use a custom binding, or overwrite/extend the textInput and/or value bindings to initialize observables with values from the DOM.
// w/ custom binding
ko.bindingHandlers.prefilledText = {
init: function(el, valueAccessor) {
// set initial value
var initVal = $(el).val()
valueAccessor()(initVal)
// apply normal textInput binding
ko.applyBindingsToNode(el, {
textInput: valueAccessor()
})
}
}
// w/ extended textInput binding
// save reference to initial binding
var _textInputBinding = ko.bindingHandlers.textInput
ko.bindingHandlers.textInput = {
init: function(el, valueAccessor) {
// set initial value, same as before
var initVal = $(el).val()
valueAccessor()(initVal)
// pass on to the regular textInput binding
_textInputBinding.init.apply(this, arguments)
},
// don't forget the update function either!
update: _textInputBinding.update
}
and a fiddle!

Update KnockoutJS viewModel using mapping and localStorage to maintain persistence

I need the data saved to localStorage, "savedData", to update the currently empty view model when the page is revisted/refreshed, possibly using the KO mapping plug-in in the line:
ko.mapping.fromJS(this, retrievedData);
But of course it's not working!
function viewModel() {
var self = this;
self.employees = ko.observableArray([]);
self.removeEmployee = function (employee) {
self.employees.remove(employee);
};
self.addEmployee = function () {
self.employees.push(new Employee());
};
self.save = function () {
var savedData = ko.toJSON(this);
localStorage.setItem('savedData', savedData);
//console.log('savedData', JSON.parse(savedData))
}
if (localStorage && localStorage.getItem('savedData')) {
var retrievedData = JSON.parse(localStorage.getItem('savedData'));
ko.mapping.fromJS(this, retrievedData);
}
}
var vm = new viewModel();
ko.applyBindings(vm);
Problem is in fromJS parameters. Second parameter is mapping options but you are trying to pass data instead. Proper usage should be following
var viewModel = ko.mapping.fromJS(retrievedData, mappingOptions);
If you don't want map retreivedData to viewModel using mappingOptions
ko.mapping.fromJS(retrievedData, {}, viewModel);
This will convert your data to viewModel without any specific mapping options.
NOTE: If you call mapping.fromJS with two arguments and second argument IS mapping object (__ko_mapping__ property defined) it will treat second argument as a viewModel and you can use function in a different manner:
ko.mapping.fromJS(retrievedData, viewModel);
Basically this scenario is valid when you are not creating viewModel, but instead updating previously created using mapping viewModel with some data.
ANSWER: You should update your code co map properly by passing empty options parameter. An of course data should go first:
if (localStorage && localStorage.getItem('savedData')) {
var retrievedData = JSON.parse(localStorage.getItem('savedData'));
ko.mapping.fromJS(retrievedData, {}, self);
}

knockout validation unique in list

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();
});

Detecting change to Knockout view model

Sure this is a very easy question to answer but is there an easy way to determine if any property of a knockout view model has changed?
Use extenders:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.setOriginalValue = function(startingValue) {
target.originalValue = startingValue;
};
target.subscribe(function (newValue) {
// use != not !== so numbers will equate naturally
target.isDirty(newValue != target.originalValue);
});
}
return target;
};
Then:
self.MyProperty= ko.observable("Property Value").extend({ trackChange: true });
Now you can inspect like this:
self.MyProperty.isDirty()
You can also write some generic viewModel traversing to see if anything's changed:
self.isDirty = ko.computed(function () {
for (key in self) {
if (self.hasOwnProperty(key) && ko.isObservable(self[key]) && typeof self[key].isDirty === 'function' && self[key].isDirty()) {
return true;
}
}
});
... and then just check at the viewModel level
self.isDirty()
You can subscribe to the properties that you want to monitor:
myViewModel.personName.subscribe(function(newValue) {
alert("The person's new name is " + newValue);
});
This will alert when personName changes.
Ok, so you want to know when anything changes in your model...
var viewModel = … // define your viewModel
var changeLog = new Array();
function catchChanges(property, value){
changeLog.push({property: property, value: value});
viewModel.isDirty = true;
}
function initialiseViewModel()
{
// loop through all the properties in the model
for (var property in viewModel) {
if (viewModel.hasOwnProperty(property)) {
// if they're observable
if(viewModel[property].subscribe){
// subscribe to changes
viewModel[property].subscribe(function(value) {
catchChanges(property, value);
});
}
}
}
viewModel.isDirty = false;
}
function resetViewModel() {
changeLog = new Array();
viewModel.isDirty = false;
}
(haven't tested it - but you should get the idea)
Consider using Knockout-Validation plug-in
It implements the following:
yourProperty.isModified() - Checks if the user modified the value.
yourProperty.originalValue - So you can check if the value really changed.
Along with other validation stuff which comes in handy!
Cheers
You might use the plugin below for this:
https://github.com/ZiadJ/knockoutjs-reactor
The code for example will allow you to keep track of all changes within any viewModel:
ko.watch(someViewModel, { depth: -1 }, function(parents, child) {
alert('New value is: ' + child());
});
PS: As of now this will not work with subscribables nested within an array but a new version that supports it is on the way.
Update: The sample code was upgraded to work with v1.2b which adds support for array items and subscribable-in-subscribable properties.
I had the same problem, i needed to observe any change on the viewModel, in order to send the data back to the server,
If anyone still intersted, i did some research and this is the best solution iv'e managed to assemble:
function GlobalObserver(viewModel, callback) {
var self = this;
viewModel.allChangesObserver = ko.computed(function() {
self.viewModelRaw = ko.mapping.toJS(viewModel);
});
viewModel.allChangesObserver.subscribe(function() {
callback(self.viewModelRaw);
});
self.dispose = function() {
if (viewModel.allChangesObserver)
viewModel.allChangesObserver.dispose();
delete viewModel.allChangesObserver;
};
};
in order to use this 'global observer':
function updateEntireViewModel() {
var rawViewModel = Ajax_GetItemEntity(); //fetch the json object..
//enter validation code here, to ensure entity is correct.
if (koGlobalObserver)
koGlobalObserver.dispose(); //If already observing the older ViewModel, stop doing that!
var viewModel = ko.mapping.fromJS(rawViewModel);
koGlobalObserver = new GlobalObserver(viewModel, Ajax_Submit);
ko.applyBindings(viewModel [ ,optional dom element]);
}
Note that the callback given (in this case 'Ajax_Submit') will be fired on ANY change that occurs on the view model, so i think it's really recommended to make some sort of delay mechanism to send the entity only when the user finished to edit the properties:
var _entitiesUpdateTimers = {};
function Ajax_Submit(entity) {
var key = entity.ID; //or whatever uniquely related to the current view model..
if (typeof _entitiesUpdateTimers[key] !== 'undefined')
clearTimeout(_entitiesUpdateTimers[key]);
_entitiesUpdateTimers[key] =
setTimeout(function() { SendEntityFunction(entity); }, 500);
}
I'm new to JavaScript and the knockout framework, (only yestarday i started to work with this wonderfull framework), so don't get mad at me if i did something wrong.. (-:
Hope this helps!
I've adapted #Brett Green code and extended it so that we can have AcceptChanges, marking the model as not dirty plus having a nicer way of marking models as trackables. Here is the code:
var viewModel = {
name: ko.observable()
};
ko.track(viewModel);
http://jsfiddle.net/david_freire/3HZEu/2/
I did this by taking a snapshot of the view model when the page loads, and then later comparing that snapshot to the current view model. I didn't care what properties changed, only if any changed.
Take a snapshot:
var originalViewModel = JSON.stringify(ko.toJS(viewModel));
Compare later:
if(originalViewModel != JSON.stringify(ko.toJS(viewModel))){
// Something has changed, but we don't know what
}
Consider a view model as follows
function myViewModel(){
var that = this;
that.Name = ko.observable();
that.OldState = ko.observable();
that.NewState = ko.observable();
that.dirtyCalcultions - ko.computed(function(){
// Code to execute when state of an observable changes.
});
}
After you Bind your Data you can store the state using ko.toJS(myViewModel) function.
myViewModel.Name("test");
myViewModel.OldState(ko.toJS(myViewModel));
You can declare a variable inside your view model as a computed observable like
that.dirtyCalculations = ko.computed(function () {});
This computed function will be entered when there is change to any of the other observables inside the view model.
Then you can compare the two view model states as:
that.dirtyCalculations = ko.computed(function () {
that.NewState(that);
//Compare old state to new state
if(that.OldState().Name == that.NewState().Name()){
// View model states are same.
}
else{
// View model states are different.
}
});
**Note: This computed observable function is also executed the first time when the view model is initialized. **
Hope this helps !
Cheers!!
I like Brett Green's solution. As someone pointed out, the isDirty comparison doesn't work with Date objects. I solved it by extending the subscribe method like this:
observable.subscribe(function (newValue) {
observable.isDirty(newValue != observable.originalValue);
if (newValue instanceof Date) {
observable.isDirty(newValue.getTime() != observable.originalValue.getTime());
}
});

Categories