I am very new to knockout.js, in fact came across this two days ago when I started to realize my UI scripts were getting out of hand.
So, I have several view models, all being populated using the mapping functionality.
I have the models populating OK from the JSON that is being supplied via my MVC controller.
function ChangeViewModel(data) {
var self = this;
changeMapping = {
'CORs': {
create: function (options) {
return new CORViewModel(options.data);
}
}
}
return ko.mapping.fromJS(data, changeMapping, self);
}
function CORViewModel(data) {
var self = this;
corMapping = {
'copy': ['VendorID', 'VendorName', 'ContractID'],
'include': ['CorNo', 'CorName', 'Items'],
'Items': {
create: function (options) {
return new CORItemViewModel(options.data);
}
}
}
self.CorNoName = ko.pureComputed(function () {
if (self.CorVersion() > 1) return self.CorNo() + ":" + self.CorVersion();
return self.CorNo();
});
self.GrandTotal = ko.pureComputed(function () {
var total = 0;
$.each(self.Items(), function () { total += this.Total() })
return total;
});
return ko.mapping.fromJS(data, corMapping, self);
}
function CORItemViewModel(data) {
var self = this;
corItemMapping = {
'copy': ['ChangeItemId', 'VendorCorId', 'VendorName', 'VendorId'],
}
return ko.mapping.fromJS(data, corItemMapping, self);
}
So that's my view models and this is how I load the vm in the $(document).ready function
var vm = new ChangeViewModel(data);
ko.applyBindings(vm);
I have all this displayed in a table with inputs etc and all looks great.
But if I change a Total in the CORItemViewModel the GrandTotal in the parent CORViewModel doesn't update. Can anyone tell me what I may be missing?
I created a fiddle thx to super cools suggestion, and it worked fine...
https://jsfiddle.net/afvh6d6t/14/
code..
So I went back to the original, it turned out to be an issue with a bindingHandler I used ko-money that broke the updating, so I used this bindingHandler..
http://djsharepoint.com/post/2015/01/28/useful-knockout-js-binding-handlers
which sorted the issue.
Thanks to everyone who took the time to post.
Related
Im struggling to find a way to get the properties Override & Justification available outside of the function. The code is:
self.CasOverridesViewModel = ko.observable(self.CasOverridesViewModel);
var hasOverrides = typeof self.CasOverridesViewModel === typeof(Function);
if (hasOverrides) {
self.setupOverrides = function() {
var extendViewModel = function(obj, extend) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
extend(obj[property]);
}
}
};
extendViewModel(self.CasOverridesViewModel(), function(item) {
item.isOverrideFilledIn = ko.computed( function() {
var result = false;
if (!!item.Override()) {
result = true;
}
return result;
});
if (item) {
item.isJustificationMissing = ko.computed(function() {
var override = item.Override();
var result = false;
if (!!override) {
result = !item.hasAtleastNineWords();
}
return result;
});
item.hasAtleastNineWords = ko.computed(function() {
var justification = item.Justification(),
moreThanNineWords = false;
if (justification != null) {
moreThanNineWords = justification.trim().split(/\s+/).length > 9;
}
return moreThanNineWords;
});
item.isValid = ko.computed(function() {
return (!item.isJustificationMissing());
});
}
});
}();
}
I've tried it by setting up a global variable like:
var item;
or
var obj;
if(hasOverrides) {...
So the thing that gets me the most that im not able to grasp how the connection is made
between the underlying model CasOverridesviewModel. As i assumed that self.CasOverridesViewModel.Override() would be able to fetch the data that is written on the screen.
Another try i did was var override = ko.observable(self.CasOverridesViewModel.Override()), which led to js typeError as you cannot read from an undefined object.
So if anyone is able to give me some guidance on how to get the fields from an input field available outside of this function. It would be deeply appreciated.
If I need to clarify some aspects do not hesitate to ask.
The upmost gratitude!
not sure how far outside you wanted to go with your variable but if you just define your global var at root level but only add to it at the moment your inner variable gets a value, you won't get the error of setting undefined.
var root = {
override: ko.observable()
};
root.override.subscribe((val) => console.log(val));
var ViewModel = function () {
var self = this;
self.override = ko.observable();
self.override.subscribe((val) => root.override(val));
self.load = function () {
self.override(true);
};
self.load();
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
After much research and trail and error, I haven't come up with a solution yet. Please help! The SearchCustomer method in the code has comments on the scenarios that work and don't work.
Situation
I use knockoutjs with the mapping plugin. I take a view model which contains a Workorder from the server and it contains some properties about it along with a Customer model underneath it and a Contact model underneath Customer.
On the workorder screen the user can search for a customer which pops up a modal search window. They select that customer and the customer's id and customer model comes back to the workorder. I update the workorder's customerID no problem, but when I try to update the customer data (including contact) I get the Function Expected error.
Code
function WorkorderViewModel(data) {
var self = this;
data = data || {};
mapping = {
'Workorder': {
create: function (options) {
return new Workorder(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
self.ViewCustomer = function () {
self.Workorder.Customer.View();
}
self.SearchCustomer = function () {
self.Workorder.Customer.Search(function (customerID, customer) {
self.Workorder.CustomerID(customerID); //Works
self.Workorder.Customer(customer) //Function Expected, I feel this should work! Please help!
self.Workorder.Customer = new Customer(customer, self.Workorder); //No Error doesn't update screen
self.Workorder.Customer.Contact.FirstName(customer.Contact.FirstName); //Works, updates screen, but I don't want to do this for every property.
self.Workorder.SaveAll(); //Works, reload page and customer data is correct. Not looking to reload webpage everytime though.
})
}
}
function Workorder(data, parent) {
var self = this;
data = data || {};
mapping = {
'Customer': {
create: function (options) {
return new Customer(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Customer(data, parent) {
var self = this;
data = data || {};
mapping = {
'Contact': {
create: function (options) {
return new Contact(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Contact(data, parent) {
var self = this;
data = data || {};
mapping = {};
ko.mapping.fromJS(data, mapping, self);
self.AddedOn = ko.observable(moment(data.AddedOn).year() == 1 ? '' : moment(data.AddedOn).format('MM/DD/YYYY'));
self.FullName = ko.computed(function () {
var fullName = '';
if (self.FirstName() != null && self.FirstName() != '') {
fullName = self.FirstName();
}
if (self.MiddleName() != null && self.MiddleName() != '') {
fullName += ' ' + self.MiddleName();
}
if (self.LastName() != null && self.LastName() != '') {
fullName += ' ' + self.LastName();
}
return fullName;
})
}
Thanks Everyone!
Since self.Workorder.Customer is originally populated using ko.mapping, when you want to repopulate it, you should use ko.mapping again, like:
ko.mapping.fromJS(customer, self.Workorder.Customer)
Try changing:
self.Workorder.Customer(customer);
to:
self.Workorder.Customer = customer;
My guess is that the Customer property of the Workorder is not an observable.
How come this line of code doesnt work.
Im using durandal/knockout and i have a structure like this
define(function () {
var vm = function() {
compute: ko.computed(function() {
return _compute(1); // fail
});
var _compute= function(e) {
return e;
}
}
return vm;
});
Basically I am just trying to access the private method _compute - but KO.compute doesnt allow that?
Even if i make it public, I still cant access it.
I trying to implement revealing pattern in this, but still no luck!
var vm = function() {
compute: ko.computed(function() {
return this._compute(1); // still failing
});
this._compute= function(e) {
return e;
}
}
update: so far, only this one works
define(function () {
var vm = function() {
var self = this;
var self._compute= function(e) {
return e;
}
compute: ko.computed(function() {
return this._compute(1); // works
}, self);
}
but like I said, _compute is not meant to be exposed.
Update: actually its another error.
this one now works
define(function () {
var vm = function() {
var self = this;
var _compute= function(e) {
return e;
}
compute: ko.computed(function() {
return _compute(1); // works
});
}
Basically, just need to declare the private function before the ko.computed prop!
Thanks!
Additional Note:
Why does it need to be declared before the computed function? I prefer all my "properties" in the first lines while the functions in the bottom. It is neater i Think.
This syntax does not create a property when in a function:
compute: ko.computed(function() {
return _compute(1); // fail
});
You have to use = instead of :.
Try this
var vm = function() {
var self = this;
var _compute = function(e) {
return e;
}
this.compute = ko.computed(function() {
return _compute(1);
});
}
Also note that this is not how you should use a computed observable. It should contain calls to other observables!
From doc:
What if you’ve got an observable for firstName, and another for
lastName, and you want to display the full name? That’s where computed
observables come in - these are functions that are dependent on one or
more other observables, and will automatically update whenever any of
these dependencies change.
I'm creating a music-based app that uses Knockout for it's bindings. All of the tracks listed in the app are pulled externally from Soundcloud like so:
$.getJSON('http://api.soundcloud.com/users/guy-j/tracks.json?client_id=c4bc3b1a93902abecbaca3fa4582d970', {limit: 200}, function(data) {
vm.tracks($.map(data, function (track) {
return {
artwork: track.artwork_url,
avatar: track.user.avatar_url,
date: track.created_at,
description: track.description,
duration: track.duration,
listens: track.playback_count,
permalink: track.permalink_url,
purhcase: track.purchase_url,
stream: track.stream_url,
track: track.title
};
}));
});
These tracks (once fetched) are pushed into a blank observableArray and the HTML view is then bound to that array and generates a list of tracks. I've got the audio playing/pausing like so:
To select/play a track, each <li> that's generated from the observableArray has a click handler called 'goToTrack' which then passes an observable called 'self.chosenTrackData' the selected track and my audio element is then bound with 'chosenTrackData' to play the chosen track.
My problem now lies with the fact that I'm not quite sure what the best way to approach the next/prev track features using Knockout would be. I'd have to some how distinguish the current point in the observableArray and then ++ or -- depending which option you've selected?
Any help would be greatly appreciated as I'm still learning Knockout!
I'd suggest keeping an internal "current track" number that you increment or decrement and base a computed on that.
function PlayerViewModel() {
var self = this,
currentTrackNo = ko.observable(0);
// data
self.tracks = ko.observableArray();
// computeds
self.currentTrack = ko.computed(function () {
return self.tracks()[currentTrackNo()];
});
self.hasTrack = function (track) {
return !!self.currentTrack();
};
self.hasNextTrack = ko.computed(function () {
return currentTrackNo() < self.tracks().length - 1;
});
self.hasPrevTrack = ko.computed(function () {
return currentTrackNo() > 0;
});
// API
self.setNextTrack = function () {
if (self.hasNextTrack()) {
currentTrackNo(currentTrackNo() + 1);
}
};
self.setPrevTrack = function () {
if (self.hasPrevTrack()) {
currentTrackNo(currentTrackNo() - 1);
}
};
self.setTrack = function (track) {
var trackNo = ko.utils.arrayIndexOf(self.tracks(), track);
currentTrackNo(trackNo);
};
// init
$.getJSON('...', {limit: 200}); etc;
}
Bind your "current track" view component to the currentTrack observable. Make sure that you use hasTrack in some way (as currentTrack might be undefined).
Alternative implementation, avoiding the maintenance of a separate track number:
function PlayerViewModel() {
var self = this;
// data
self.tracks = ko.observableArray();
self.currentTrack = ko.observable();
// computeds
self.currentTrackNo = ko.computed(function () {
var currentTrack = self.currentTrack();
return ko.utils.arrayIndexOf(self.tracks(), currentTrack);
});
self.hasTrack = function (track) {
return !!self.currentTrack();
};
self.hasNextTrack = ko.computed(function () {
return self.currentTrackNo() < self.tracks().length - 1;
});
self.hasPrevTrack = ko.computed(function () {
return self.currentTrackNo() > 0;
});
// API
self.setNextTrack = function () {
if (self.hasNextTrack()) {
self.currentTrack(self.tracks()[self.currentTrackNo() + 1]);
}
};
self.setPrevTrack = function () {
if (self.hasPrevTrack()) {
self.currentTrack(self.tracks()[self.currentTrackNo() - 1]);
}
};
self.setTrack = function (track) {
// make sure that track is actually in self.tracks here
self.currentTrack(track);
};
// init
$.getJSON('...', {limit: 200}); etc;
}
I have the following:
// Child Array is Cards, trying to add computed observable for each child
var CardViewModel = function (data) {
ko.mapping.fromJS(data, {}, this);
this.editing = ko.observable(false);
};
var mapping = {
'cards': { // This never gets hit, UNLESS I remove the 'create' method below
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
},
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data);
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
var SetViewModel = ko.mapping.fromJS(setData, mapping);
debugger;
ko.applyBindings(SetViewModel);
However I can't get the 'cards' binding to work - that code isn't reached unless I remove the 'create' method. I'm trying to follow the example from the knockout site:
http://knockoutjs.com/documentation/plugins-mapping.html
They do this for the child object definition:
var mapping = {
'children': {
create: function(options) {
return new myChildModel(options.data);
}
}
}
var viewModel = ko.mapping.fromJS(data, mapping);
With the ChildModel defined like this:
var myChildModel = function(data) {
ko.mapping.fromJS(data, {}, this);
this.nameLength = ko.computed(function() {
return this.name().length;
}, this);
}
I've spent the past day on this and cannot for the life of me figure out why this isn't working. Any tips would be awesome.
EDIT: Here's a fiddle of what I'm working with. It's only showing SIDE 1 in the result because "editing" isn't recognized here:
<div data-bind="visible: !$parent.editing()" class="span5 side-study-box">
http://jsfiddle.net/PTSkR/1/
This is the error I get in chrome when I run it:
Uncaught Error: Unable to parse bindings. Message: TypeError: Object
has no method 'editing'; Bindings value: visible: !$parent.editing()
You have overridden the create behavior for your view model. The mapping plugin will not call any of the other handlers for the properties for you. Since you're mapping from within the create method, move your cards handler in there.
var mapping = {
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data, {
'cards': {
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
}
});
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
updated fiddle
you didnt needed to have parenthesis. I just changed from
!$parent.editing()
to
!$parent.editing
See the updated fiddle here