Knockout: observableArray of arrays of observable inputs - javascript

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

Related

How to extend a mapped knockout viewmodel "on the fly" with additional property?

I'm getting a json object array from my ASP.NET Web API and map it to my knockout viewmodel (ko.observablearray). While binding the model to the document I get an "Unable to process binding" error.
This is because the json dto I get from the API doesen't have the Editable property defined. How can I add it on the fly while mapping? It should be false by default.
Here's the code:
var Accessory = function (id, leftOnPatient, reject, amount, patchNumber, additionalInfo, editable) {
var self = this;
self.Id = ko.observable(id);
self.LeftOnPatient = ko.observable(leftOnPatient);
self.Reject = ko.observable(reject);
self.Amount = ko.observable(amount);
self.PatchNumber = ko.observable(patchNumber);
self.AdditionalInfo = ko.observable(additionalInfo);
self.Editable = ko.observable(editable);
}
function MedicinesAndAccessoriesViewModel() {
var self = this;
self.Accessories = ko.observableArray([]);
self.error = ko.observable();
function getAllAccessories() {
ajaxHelper(accessoriesUri, 'GET').done(function (data) {
ko.mapping.fromJS(data, {}, self.Accessories);
});
};
getAllAccessories();
};
var vm = new MedicinesAndAccessoriesViewModel();
ko.applyBindings(vm, document.getElementById("medicinesAndAccessoriesTab"));
HTML:
<ol class="list-group list_of_items padding-top-15" data-bind="foreach: Accessories">
<li class="list-group-item">
<div class="container-fluid">
<div class="col-md-4">
<span data-bind="text: Id, visible: !Editable()"></span> //Here I get the error
....
...
..
.
So, how can I define the Editable(false) property client side for each Accessory loaded from the API? Every other property I get from the dto loaded from the API. Can it be done somehow while mapping?
You're currently not using the Accessory constructor function anywhere at all, the word occurs only once at its definition in your snippet...
You need to utilize your mapping definition and tell it to map that part of your json correctly, e.g.:
function getAllAccessories() {
ajaxHelper(accessoriesUri, 'GET').done(function (data) {
var mappingRules = {
'accessories': {
create: function(options) {
return new Accessory(
options.data.id,
options.data.leftOnPatient,
options.data.reject,
options.data.amount,
options.data.patchNumber,
options.data.additionalInfo,
options.data.editable);
}
}
};
ko.mapping.fromJS(data, mappingRules, self.Accessories);
});
};
This should already make sure the Editable observable is there. You could try to force it into a boolean with !! just in case:
self.Editable = ko.observable(!!editable);
PS. I recommend making the constructor function slightly less complicated by reducing the number of arguments to one (e.g. data or dto), so that return new Accessory(options.data) will do in the above snippet.

How do you use knockout mapping to map from a null value to an empty observable array?

I have a problem when I'm trying to use knockout mapping to map from a null json property to a knockout observable array. After the mapping has been performed, the knockout array is null.
So, for example, I have the following data:
var data = { 'Name': 'David', 'Modules': null };
And the following models:
var ModuleViewModel = function(data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
};
var Model = function(data) {
var self = this;
var mapping = {
'Modules': {
key: function(module) {
return ko.utils.unwrapObservable(module.Id);
},
create: function (options) {
return new ModuleViewModel(options.data);
}
}
};
ko.mapping.fromJS(data, mapping, self);
};
Which is then applied to the view using:
var model = new Model(data);
ko.applyBindings(model);
And in the view, simply:
<span data-bind="text: Name"></span>
<span data-bind="text: Modules().length"></span>
What I would like to see (after my name) is the number of modules that have been mapped. If modules were present in the data:
var data = { 'Name': 'David', 'Modules': [ {'Id': '1', 'Name': 'Module1'} ] }
then the correct number is displayed on the view, but if the modules are set to null in the data then the number is not displayed at all as it does not know to map to an observable array.
Additionally, if I define an extra property on my view model to map to:
self.Modules = ko.observableArray;
this still does not get mapped as I expect as if you query self.Modules() after the mapping you get a null value returned when if you query a normal empty observable array you get [] returned.
What am I doing wrong in the mapping? I would just like a 0 value to be displayed when the modules data is null.
Click here to edit using jsbin
Here is a little update to your mappings that you might find useful.
http://jsbin.com/zeguxiveyi/1
It's important to note the following changes.
For one, you weren't applying your mapping.
// need to invoke the mapping object
ko.mapping.fromJS(data, mapping, self);
Secondly, it doesn't make a lot of sense to invoke an observable with null...it's like eating the hole of a donut, right?
//Initialize your data with null modules :: ERROR
//For the sake of having an empty observable array, we need to convert this null to an empty array
var data = { 'Name': 'David', 'Modules': null };
if(data.Modules == null) data.Modules = [];
Thirdly, in the case where you may be getting nulls, you should go ahead and add a little short-circuit logic to prevent it...
var mapping = {
'Modules': {
create: function (options) {
if(options.data !== null)
return new ModuleViewModel(options.data);
}
}
};
All in all, the mapping plugin is useful, but there's nothing magical about it...still need to be sure your data is reasonable.
You could put a computed observable for null checking and to keep it as clean as possible, like this:
var ModuleViewModel = function(data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
//After mapping the object, add the computed observable here
self.numberOfModules = ko.computed(function() {
return this.Modules ? this.Modules().length : 0;
}, self);
};
Then in your binding:
<span data-bind="text: numberOfModules">
This is specially handy if the computed is used in many different places in the HTML in order to avoid duplicating null checks everywhere.

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

Knockout.JS Observable Array via AJAX bound to DropDownList

I am trying to fill my view model with some drop down options from the database. I want to keep track of the selected object because it has properties on it that I am using in a custom binding elsewhere.
I initialize the observable with a "blank" value so that it's properly set when the binding occurs and my custom binding works. Once the server responds, I morph the data to an observable array and the drop down list displays the options.
Here is the JavaScript code:
ko.bindingHandlers.jq_fade = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.utils.unwrapObservable(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.utils.unwrapObservable(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
function Firm(id, name, outsideCounsel) {
this.name = name;
this.id = id;
this.outsideCounsel = outsideCounsel;
}
function RequestViewModel() {
var self = this,
ONE_DAY = 1000 * 60 * 60 * 24;
self.firm = ko.observable(new Firm(-1, "", false));
$.post(ajaxAddress + 'LoadFirms', function (data) {
var mappedFirms = $.map(data.d, function (item) {
return new Firm(item.OrganizationLookupID, item.OrganizationLookupName, item.IsExternalCounselFirm);
});
self.availableFirms(mappedFirms);
self.firm(self.availableFirms()[0]);
});
}
$(document).ready(function () {
model = new RequestViewModel();
ko.applyBindings(model);
});
Here is the relevant HTML
<span data-bind="jq_fade: firm().outsideCounsel">
Outside Counsel
<input type="text" id="outsideCounsel" data-bind="value: outsideCounsel" />
</span>
I want that DIV to show only if the selected firm is an outside counsel. If remove the line data-bind="jq_fade: firm().outsideCounsel, everything works fine. If I make the $.post() calls synchronously, it works. I'm thinking it's something wrong with my init function in jq_fade.
The error I receive is:
Uncaught Error: Unable to parse bindings.
Message: TypeError: Cannot call method 'outsideCounsel' of undefined;
Bindings value: jq_fade: firm().outsideCounsel()
I understand what Knockout is telling me, I'm just not sure how firm() can ever be undefined because I set up an initial value.
If you're binding availableFirms() to a dropdownlist, I'm assuming you've also bound firm() to the same list so that when another is selected from the list, firm() gets automatically updated and all your bindings update automatically.
If this is the case, you do not need to set firm() at all as it will be set to the first element in the dropdownlist anyway.
See example 3 here:
http://knockoutjs.com/documentation/options-binding.html
var viewModel = {
availableCountries : ko.observableArray([
new country("UK", 65000000),
new country("USA", 320000000),
new country("Sweden", 29000000)
]),
selectedCountry : ko.observable() // Nothing selected by default
};
Try it like above without specifically setting firm() and see if it errors again

Categories