How can I set an observable property without any subscriptions firing for it?
I have a scenario were the page loads, an ajax call is made to get some data, the data is looped over and the currently selected item is then set to an observable. I want to be able to set this observable without any subscriptions for it firing because the first time this observable is set is considered its initial sate and the subscriptions should not execute on initial state.
function PlanViewModel() {
var self = this;
self.plans = ko.observableArray();
self.selectedItem = ko.observable();
self.getAllPlans = function () {
$.ajax({
url: "/Backoffice/Home/GetAllPlans",
type: "POST",
data: {},
context: this,
success: function (result) {
var planList = this.plans;
// clear the plan list
planList.removeAll();
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
self.selectedItem(planDetail); // how do I set this without the subscriptions firing?
}
planList.push(planDetail);
});
},
error: function (result) {
alert("An error occured getting plans.");
}
});
}
self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
} .bind(self));
}
You could restructure your code like this:
function PlanViewModel() {
var self = this;
self.plans = ko.observableArray();
self.getAllPlans = function () {
$.ajax({
// …
success: function (result) {
// …
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
self.selectedItem = ko.observable(planDetail);
}
planList.push(planDetail);
});
if (self.selectedItem === undefined) {
self.selectedItem = ko.observable();
}
self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
}.bind(self));
},
// …
});
}
}
That is, only start Knockout after your desired initial state is achieved.
Thanks, I went down that route and its working with some modifications. The selectedItem observable must be defined on the model from the get go because its used in bindings all over the place but I did move the subscription portion like you've suggested and that's working out great.
function PlanViewModel() {
var self = this;
var selectedItemSubscription = null;
self.plans = ko.observableArray();
self.selectedItem = ko.observable();
self.getAllPlans = function () {
$.ajax({
url: "/Backoffice/Home/GetAllPlans",
type: "POST",
data: {},
context: this,
success: function (result) {
var planList = this.plans;
// clear the plan list
planList.removeAll();
$.each(result.plans, function () {
var planDetail = new PlanDetail(this, self);
if (this.IsSelected) {
if (selectedItemSubscription != null)
selectedItemSubscription.dispose();
self.selectedItem(planDetail);
}
planList.push(planDetail);
});
selectedItemSubscription = self.selectedItem.subscribe(function (newItem) {
newItem.repositoryUpdateSelectedPlan();
}.bind(self));
},
error: function (result) {
alert("An error occured getting plans.");
}
});
}
}
Related
var ChannelStatsView = Backbone.View.extend({
el: "#id-channel-stats",
initialize: function() {
var _this = this;
this.modelChannelList = new channelListModel();
this.modelChannelStats = new channelStatsModel();
this.channelstatsCollection = new channelStatsCollection();
this.channelNames = [];
this.listOfObjects = [];
this.modelChannelList.fetch({
success: function(model, response, options) {
model.set();
_this.formatChannelIds();
},
error: function(model, xhr, options) {
}
});
},
formatChannelIds: function() {
_this = this;
_.filter(_this.modelChannelList.toJSON(), function(channelObj) {
if (channelObj['isactive'] == true) {
_this.updateStats(channelObj['id'], channelObj['name']);
}
});
},
updateStats: function(id, name) {
var _this = this;
_this.modelChannelStats.fetch({
data: {
channel: id
},
processData: true,
success: function(model, response, options) {
_this.response = response;
_this.listOfObjects.push(_this.response.records[0]);
_this.channelNames.push(name);
}
}).done(function(model, response, options) {
_this.render();
});
},
render: function() {
var _this = this;
if (_this.listOfObjects.length == 0) {
} else {
_this.template = channelTemplate;
_this.$el.html(_this.template({
orderData: _this.listOfObjects,
channelNames: _this.channelNames
}));
}
}
});
In my code i am taking the response from one model.fetch query i.e this.modelChannelList and getting all the active id's then supplying it to another fetch to get the response i know this solution is really bad can someone help me how to make it faster and effective.
I am considering using Promises
The main issue you need to deal with here is the number of fetch requests that you are making. Promises are cool so I've included that too. Here's what I recommend you do:
1) Update your model class to assign the fetch function as a deferred
var channelListModel = Backbone.Model.extend({
initialize: function() {
// Assign the Deferred issued by fetch() as a property
this.deferred = this.fetch();
}
});
2) Modify your updateStats/formatChannels logic to create an array of ids and pass those through your fetch to get a complete data set. This will save tons of time by reducing the number of calls you have to make
initialize: function() {
// other stuff here...
this.modelChannelList.deferred.done(function(model) {
model.set();
view.formatChannelIds();
});
// other stuff here...
}
formatChannelIds: function() {
var _this = this,
ids = [];
_.filter(_this.modelChannelList.toJSON(), function(channelObj) {
if (channelObj['isactive'] == true) {
ids.push(channelObj['id']);
}
_this.updateStats(ids);
});
}
You will have to change up your data service a bit, but this is a change that is ultimately necessary anyways.
I have a Grandparent, Parent, Child ViewModel relationship setup in knockout and knockout mapping, CustomerViewModel, WorkOrderViewModel, and RepairViewModel.
For each level I flag if the record has been Modified. Then I have a save button that saves the entire Model. The function that Saves the Model is within the Grandparent ViewModel (CustomerViewModel)
Example of a Child level element
<input class="form-control input-sm text-right" name="RepairCost" id="RepairCost" data-bind="value: RepairCost, event: {change: flagRepairAsEdited}" />
Is there a way within the flagRepairAsEdited function I can call the SAVE function within the parent/grandparent?
Thanks so much!
Here is the JS code I'm using (simplified):
var ObjectState = {
Unchanged: 0,
Added: 1,
Modified: 2,
Deleted: 3
};
var workOrderMapping = {
'WorkOrders': {
key: function (workOrders) {
return ko.utils.unwrapObservable(workOrders.WorkOrderId);
},
create: function (options) {
return new WorkOrderViewModel(options.data);
}
},
'Repairs': {
key: function (repairs) {
return ko.utils.unwrapObservable(repairs.RepairId);
},
create: function (options) {
return new RepairViewModel(options.data);
}
}
};
RepairViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagRepairAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
WorkOrderViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.flagWorkOrderAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
//WOULD LOVE TO CALL SAVE FUNCTION HERE
return true;
}
;
}
CustomerViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, workOrderMapping, self);
self.save = function () {
$.ajax({
url: "/Customers/Save/",
type: "POST",
data: ko.toJSON(self),
contentType: "application/json",
success: function (data) {
ko.mapping.fromJS(data.customerViewModel, workOrderMapping, self);
if (data.newLocation != null)
window.location = data.newLocation;
},
});
},
self.flagCustomerAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
return true;
}
;
}
There are 2 ways to do this
a) Pass viewModels as parameters of the child flagRepairAsEdited function:
data-bind="value: RepairCost, event: {change: flagRepairAsEdited.bind($data, $parent, $root)}"
b) Save the link of the parent viewModel inside child viewModel
WorkOrderViewModel = function (data, parent) {
this.parent = parent;
...
}
And use parent.flagWorkOrderAsEdited and parent.parent.flagWorkOrderAsEdited to save parent and grandparent viewmodels
Here i have my get method that gets the data that i want to return in order to bind it with the view page. I am having trouble wrapping my head to how i could bind this information to the view.
Get Method:
var getRoster = function () {
Ajax.Get({
Url: ....,
DataToSubmit: {id: properties.Id },
DataType: "json",
OnSuccess: function (roleData, status, jqXHR) {
console.log("roles:", roleData.length);
Ajax.Get({
Url: ...,
DataToSubmit: { pageNumber: 1, id: properties.Id },
DataType: "json",
OnSuccess: function (userData, status, jqXHR) {
for (var x in roleData)
{
var role = roleData[x];
console.log(role);
for (var y in userData)
{
var user = userData[y];
if (user.ContentRole == role.ContentRole)
{
rosterViewModel.PushUser(new userViewModel(user));
console.log(user);
}
}
roleTypesViewModel.PushRole(new roleViewModel(role));
}
}
});
}
});
rosterViewModel.PushUser = function (user) {
viewModel.RosterUsers.push(new userViewModel(user));
};
roleTypesViewModel.PushRole = function (role) {
viewModel.RosterRoleTypes.push(new roleViewModel(role));
}
var userViewModel = function (data) {
var _self = this;
_self.ID = ko.observable(data.ID);
_self.Name = ko.observable(data.Name);
_self.Email = ko.observable(data.Email);
_self.ContentRole = ko.observable(data.ContentRole);
};
var roleViewModel = function (data) {
var _self = this;
_self.ContentRole = ko.observable(data.ContentRole);
_self.RoleName = ko.observable(data.RoleName);
_self.RoleRank = ko.observable(data.RoleRank);
_self.UserCount = ko.observable(data.UserCount);
};
var viewModel = {
RosterRoleTypes: ko.observableArray([]),
RosterUsers: ko.observableArray([])
};
View:
<div id="gridView" data-bind="foreach: RosterRoleTypes">
<h3 class="roleHeader"><span data-bind="text:RoleName"></span>
<span class="userCount">(<span data-bind="text:UserCount"></span>)</span>
</h3>
<div data-bind="template: { name: 'grid', foreach: RosterUsers}">
</div>
</div>
How can i bind my data to display in my view?
If you are trying to bind multiple areas of your page to different view models, that is possible by passing in an additional parameter to your ko.applyBindings() method that you call. Your problem is that you are mixing models and view models and using them improperly. If you want to have one view model adjust your code to include all of the functions of your view model and set your models as models instead of viewmodels -
function rosterViewModel() {
var self = this;
self.RosterRoleTypes = ko.observableArray([]),
self.RosterUsers = ko.observableArray([])
self.PushUser = function (user) {
viewModel.RosterUsers.push(new userModel(user));
};
self.PushRole = function (role) {
viewModel.RosterRoleTypes.push(new roleModel(role));
};
self.getRoster = function () {
Ajax.Get({
Url: ....,
DataToSubmit: {id: properties.Id },
DataType: "json",
OnSuccess: function (roleData, status, jqXHR) {
Ajax.Get({
Url: ...,
DataToSubmit: { pageNumber: 1, id: properties.Id },
DataType: "json",
OnSuccess: function (userData, status, jqXHR) {
for (var x in roleData)
{
var role = roleData[x];
for (var y in userData)
{
var user = userData[y];
if (user.ContentRole == role.ContentRole)
{
self.PushUser(new userModel(user));
}
}
self.PushRole(new roleModel(role));
}
}
});
}
});
}
var userModel = function (data) {
var _self = this;
_self.ID = ko.observable(data.ID);
_self.Name = ko.observable(data.Name);
_self.Email = ko.observable(data.Email);
_self.ContentRole = ko.observable(data.ContentRole);
};
var roleModel = function (data) {
var _self = this;
_self.ContentRole = ko.observable(data.ContentRole);
_self.RoleName = ko.observable(data.RoleName);
_self.RoleRank = ko.observable(data.RoleRank);
_self.UserCount = ko.observable(data.UserCount);
};
ko.applyBindings(new rosterViewModel());
This assumes you want to use a single view model for your view. If you are combining multiple content areas that should be bound separately you can create two view models and merge them as shown in this question - KnockOutJS - Multiple ViewModels in a single View - or you could also bind them separately by passing in an additional parameter to the ko.applyBindings() method as showm here - Example of knockoutjs pattern for multi-view applications
All of the data that you want to bind to UI will be properties of your viewmodel as KO observable or observable arrays. Once the view model is created and its members are assigned with data(callbacks in your case), you need to apply bindings using ko.applyBindinds so that the data is bound to UI. In your case the last AJAX success callback seems to be the appropriate place.
Also your HTML makes using of template bindings however apparently there is no template defined with name 'grid'. Check on this.
Knockout tutorial link http://learn.knockoutjs.com/#/?tutorial=intro
Add
ko.applyBindings(viewModel);
somewhere in your application.
I'm using the get backbone method on a collection but in the same file (router),in a function works while in other function doesn't work.Below the function where doesn't works
var Models = {};
var AppRouter = Backbone.Router.extend({
routes: {
"": "home",
"user/:id":"userDetails",
"settings":"settings",//mettere id dell utente loggato
"friends":"friends",
"mailbox":"mailbox",
"landscape":"landscape",
"gestione_richieste_amic":"gestione_richieste_amic"
},
friends: function(){
console.log("friend_router");
var self=this;
Models.utenti = new Usercollection();
Models.utenti.fetch({
success: function(object) {
console.log(object);
var view=new FriendsView({model:object});
self.changePage(view);
},
error: function(amici, error) {
}
});
console.log(Models.utenti);
var cur_user=Parse.User.current().id;
console.log(Models.utenti.get(cur_user));<--undefined, don't works here
console.log(cur_user);
} ,
The reason for this the Asynchronous nature of Ajax (fetch method).
The line where you log to the console will be executed before the collection is fetched. So you see an error.
1st Option - resolving the error is moving the log to inside of the success handler
friends: function () {
console.log("friend_router");
var self = this,
Models.utenti = new Usercollection();
Models.utenti.fetch({
success: function (object) {
console.log(object);
var view = new FriendsView({
model: object
});
self.changePage(view);
console.log(Models.utenti);
var cur_user = Parse.User.current().id;
console.log(Models.utenti.get(cur_user));
console.log(cur_user);
},
error: function (amici, error) {
}
});
},
2nd Option - you might take is to bind a sync event on the collection..
initialize: function () {
this.Models.utenti = new Usercollection();
this.listenTo(this.Models.utenti, 'sync', this.logCollection);
_.bindAll(this, 'logCollection');
},
logCollection: function () {
console.log(this.Models.utenti);
var cur_user = Parse.User.current().id;
console.log(this.Models.utenti.get(cur_user));
console.log(cur_user);
},
friends: function () {
console.log("friend_router");
var self = this;
this.Models.utenti.fetch({
success: function (object) {
console.log(object);
var view = new FriendsView({
model: object
});
self.changePage(view);
},
error: function (amici, error) {
}
});
},
I have an object
var actions = {
'photos': function()
{
var self = this; // self = actions
$.get('./data.php?get=photos', function(data)
{
self.result = data;
});
},
'videos': function()
{
var self = this;
$.get('./data.php?get=videos', function(data)
{
self.result = data;
});
}
};
Each function creates one more item in actions called result
Then, instead of switch I use this (works good):
if (actions[action])
{
actions[action](); // call a function
console.log(actions);
console.log(actions.result);
}
action is a variable with value photos or videos.
console.log(actions) gives this:
Object
message: function ()
messages: function ()
profile: function ()
profile-edit: function ()
result: "<div>...</div>"
__proto__: Object
So I think there is resultitem in actions with the value "<div>...</div>".
But, console.log(actions.result) returns undefined.
Why?
I know all this code may be rewrited, but I would like to understand the reason of undefined.
Because we are dealing with asynchronous requests, we use "callbacks".
A callback is called when an asynchronous request is ready. Your request will get a response, and you send that response with the callback. The callback handles the response.
var actions = {
'photos': function(callback)
{
$.get('./data.php?get=photos', callback);
},
'videos': function(callback)
{
$.get('./data.php?get=videos', callback);
}
};
var action = 'photos';
actions[action](function(data) {
console.log(data);
});
Since you ensist on keeping the values, I would use this structure:
var actions = {
'photos': function()
{
$.get('./data.php?get=photos', function() {
this.__callback('photos', data);
});
},
'videos': function()
{
$.get('./data.php?get=videos', function() {
this.__callback('videos', data);
});
},
'__callback': function(action, data) {
this.results[action].push(data);
},
'results': {
'photos': [],
'videos': []
}
};
var action = 'photos';
actions[action]();
// use a timeout because we are dealing with async requests
setTimeout(function() {
console.log(actions.results); // shows all results
console.log(actions.results.photos); // shows all photos results
console.log(actions.results.videos); // shows all videos results
}, 3000);
gaaah what a horrible piece of code...