Knockout, how to subscribe to every change in observableArray - javascript

can you help me to get subscription on every change of my observable collection and on every item change. Didn't find information on http://knockoutjs.com/documentation/observableArrays.html
$(document).ready(function () {
var Item = function (isSelected, isEnabled, errorState,
name, group, processed, errors, state) {
var self = this;
self._isSelected = ko.observable(isSelected);
self._isEnabled = ko.observable(isEnabled);
self._errorState = ko.observable(errorState);
self._name = ko.observable(name);
self._group = ko.observable(group);
self._processed = ko.observable(processed);
self._errors = ko.observable(errors);
self._state = ko.observable(state);
};
function ViewModel() {
var self = this;
self.SentinelList= ko.observableArray([
ko.observable(new Item(false, false, false, 'Mail1', 'Mailing', 4, 0, 1)),
ko.observable(new Item(false, false, false, 'Ident1', 'Identity', 5, 0, 0)),
ko.observable(new Item(false, false, false, 'Cook', 'Look', 2, 0, 1))]);
}
var vm = new ViewModel();
for (var item in vm.SentinelList) {
item.subscribe(function () {
console.log('List changed');
});
}
ko.applyBindings(vm);
});

You can use the subscribe againt the array :
self.SentinelList.subscribe(function (changes) {
changes.forEach(function (change) {
if (change.status === 'added') {
console.log('new item !!');
change.value.subcriptions.push(change.value.subscribe(event));
} else if (change.status === 'deleted') {
ko.utils.arrayForEach(change.value.subcriptions, function(s) {
if(s) s.dispose();
}
);
console.log('deleted item !!');
}
});
}, null, "arrayChange");
See fiddle

You can use external plugin that tracks changes of view model. For example KO-Reactor
https://github.com/ZiadJ/knockoutjs-reactor
in this case subscription will look like
for(var i = 0; i < vm.SentinelList().length; i++){
ko.watch(vm.SentinelList()[i], { recurse: true }, function(params, modifiedProperty) {
console.log('SentinelList changed');
});
}
JSFIDDLE

Related

SAPUI5 only update specific binding

In SAPUI5 I have a Model ("sModel") filled with metadata.
In this model I have a property "/aSelectedNumbers".
I also have a panel, of which I want to change the visibility depending on the content of the "/aSelectedNumbers" property.
update
first controller:
var oModelMeta = cv.model.recycleModel("oModelZAPRegistratieMeta", that);
//the cv.model.recycleModel function sets the model to the component
//if that hasn't been done so already, and returns that model.
//All of my views are added to a sap.m.App, which is returned in the
//first view of this component.
var aSelectedRegistratieType = [];
var aSelectedDagdelen = ["O", "M"];
oModelMeta.setProperty("/aSelectedRegistratieType", aSelectedRegistratieType);
oModelMeta.setProperty("/aSelectedDagdelen", aSelectedDagdelen);
First Panel (Which has checkboxes controlling the array in question):
sap.ui.jsfragment("fragments.data.ZAPRegistratie.Filters.RegistratieTypeFilter", {
createContent: function(oInitData) {
var oController = oInitData.oController;
var fnCallback = oInitData.fnCallback;
var oModel = cv.model.recycleModel("oModelZAPRegistratieMeta", oController);
var oPanel = new sap.m.Panel( {
content: new sap.m.Label( {
text: "Registratietype",
width: "120px"
})
});
function addCheckBox(sName, sId) {
var oCheckBox = new sap.m.CheckBox( {
text: sName,
selected: {
path: "oModelZAPRegistratieMeta>/aSelectedRegistratieType",
formatter: function(oFC) {
if (!oFC) { return false; }
console.log(oFC);
return oFC.indexOf(sId) !== -1;
}
},
select: function(oEvent) {
var aSelectedRegistratieType = oModel.getProperty("/aSelectedRegistratieType");
var iIndex = aSelectedRegistratieType.indexOf(sId);
if (oEvent.getParameters().selected) {
if (iIndex === -1) {
aSelectedRegistratieType.push(sId);
oModel.setProperty("/aSelectedRegistratieType", aSelectedRegistratieType);
}
} else {
if (iIndex !== -1) {
aSelectedRegistratieType.splice(iIndex, 1);
oModel.setProperty("/aSelectedRegistratieType", aSelectedRegistratieType);
}
}
// arrays update niet live aan properties
oModel.updateBindings(true); //******** <<===== SEE HERE
if (fnCallback) {
fnCallback(oController);
}
},
width: "120px",
enabled: {
path: "oModelZAPRegistratieMeta>/bChanged",
formatter: function(oFC) {
return oFC !== true;
}
}
});
oPanel.addContent(oCheckBox);
}
addCheckBox("Presentielijst (dag)", "1");
addCheckBox("Presentielijst (dagdelen)", "2");
addCheckBox("Uren (dagdelen)", "3");
addCheckBox("Tijd (dagdelen)", "4");
return oPanel;
}
});
Here is the panel of which the visibility is referred to in the question. Note that it DOES work after oModel.updateBindings(true) (see comment in code above), but otherwise it does not update accordingly.
sap.ui.jsfragment("fragments.data.ZAPRegistratie.Filters.DagdeelFilter", {
createContent: function(oInitData) {
var oController = oInitData.oController;
var fnCallback = oInitData.fnCallback;
var oModel = cv.model.recycleModel("oModelZAPRegistratieMeta", oController);
var oPanel = new sap.m.Panel( {
content: new sap.m.Label( {
text: "Dagdeel",
width: "120px"
}),
visible: {
path: "oModelZAPRegistratieMeta>/aSelectedRegistratieType",
formatter: function(oFC) {
console.log("visibility");
console.log(oFC);
if (!oFC) { return true; }
if (oFC.length === 0) { return true; }
return oFC.indexOf("2") !== -1;
}
}
});
console.log(oPanel);
function addCheckBox(sName, sId) {
var oCheckBox = new sap.m.CheckBox( {
text: sName,
selected: {
path: "oModelZAPRegistratieMeta>/aSelectedDagdelen",
formatter: function(oFC) {
if (!oFC) { return false; }
console.log(oFC);
return oFC.indexOf(sId) !== -1;
}
},
select: function(oEvent) {
var aSelectedDagdelen = oModel.getProperty("/aSelectedDagdelen");
var iIndex = aSelectedDagdelen.indexOf(sId);
if (oEvent.getParameters().selected) {
if (iIndex === -1) {
aSelectedDagdelen.push(sId);
oModel.setProperty("/aSelectedDagdelen", aSelectedDagdelen);
}
} else {
if (iIndex !== -1) {
aSelectedDagdelen.splice(iIndex, 1);
oModel.setProperty("/aSelectedDagdelen", aSelectedDagdelen);
}
}
if (fnCallback) {
fnCallback(oController);
}
},
enabled: {
path: "oModelZAPRegistratieMeta>/bChanged",
formatter: function(oFC) {
return oFC !== true;
}
},
width: "120px"
});
oPanel.addContent(oCheckBox);
}
addCheckBox("Ochtend", "O", true);
addCheckBox("Middag", "M", true);
addCheckBox("Avond", "A");
addCheckBox("Nacht", "N");
return oPanel;
}
});
The reason that the model doesn´t trigger a change event is that the reference to the Array does not change.
A possible way to change the value is to create a new Array everytime you read it from the model:
var newArray = oModel.getProperty("/aSelectedNumbers").slice();
// do your changes to the array
// ...
oModel.setProperty("/aSelectedNumbers", newArray);
This JSBin illustrates the issue.

AngularJS - ng-bind not updating

I have a controller which has a function to get some alerts from an API and update a count on the front-end of my site which is bound to the alert.
Unfortunately the ng-bind attribute I'm using doesn't seem to be updating the count live, even though a simple console.log() is telling me that the actual alert count is being updated in the controller.
Front-end
<div class="modeSelector modeSelector_oneUp" data-ng-controller="MyLivestockController as vm">
<a class="modeSelector-mode" data-ui-sref="my-livestock">
<div class="modeSelector-type">Alerts</div>
<img class="modeSelector-icon" src="/inc/img/_icons/envelope-black.svg" onerror="this.src=envelope-black.png" />
<span data-ng-bind="vm.alertCount"></span>
</a>
</div>
Controller
(function() {
'use strict';
function MyLivestockController(userService) {
var vm = this;
vm.myLivestockNotification = {
isLoading: true,
hasError: false
};
vm.alertsNotification = {
isLoading: true,
hasError: false,
hasData: false
};
vm.deleteAlert = function(id) {
vm.currentAlert = void 0;
vm.alertsNotification.isLoading = true;
userService.deleteAlert(vm.user.id, id).then(function() {
// Remove the alert from our Array
vm.alerts = vm.alerts.filter(function(alert) {
return alert.id !== id;
});
// Refresh the alert count for the user
vm.getAlerts(vm.user.id);
vm.alertsNotification.isLoading = false;
vm.alertsNotification.hasError = false;
}, function() {
vm.alertsNotification.hasError = true;
});
};
vm.getAlerts = function(id) {
userService.getAlerts(id).then(function(alertData) {
vm.alertCount = alertData.length;
if (vm.alertCount > 0) {
vm.alertsNotification.hasData = true;
} else {
vm.alertsNotification.hasData = false;
}
vm.alerts = alertData;
vm.alertsNotification.isLoading = false;
vm.alertsNotification.hasError = false;
}, function() {
vm.alertsNotification.hasError = true;
});
};
// Init
(function() {
userService.getCurrentUser().then(function(data) {
vm.myLivestockNotification.hasError = false;
vm.myLivestockNotification.isLoading = false;
vm.user = data;
// Get alert count for the user
vm.getAlerts(vm.user.id);
}, function() {
vm.myLivestockNotification.hasError = true;
});
})();
}
angular
.module('abp')
.controller('MyLivestockController', MyLivestockController);
})();
Service
(function() {
'use strict';
function userService($q, $sessionStorage, $localStorage, $filter, user) {
var service = this;
service.getAlerts = function(id) {
var deferred = $q.defer();
user.alerts({ userID: id }, function(response) {
if (response.hasOwnProperty('data')) {
// Convert dates to valid Date
angular.forEach(response.data, function(alert) {
/* jshint camelcase: false */
if (alert.created_at) {
alert.created_at = $filter('abpDate')(alert.created_at);
/* jshint camelcase: true */
}
});
deferred.resolve(response.data);
}
else {
deferred.reject('DATA ERROR');
}
}, function(e) {
deferred.reject(e);
});
return deferred.promise;
};
angular
.module('abp')
.service('userService', userService);
})();
As you can see, I've got my getAlerts() function being called every time an alert is deleted, using the deleteAlert() function, but the <span data-ng-bind="vm.alertCount"></span> on the front-end only updates after refreshing the page, where I'd like it to update live.
Your bind is not updating because you change the value of alertCount outside of digest cycle of your angular app. When you refresh your app, the digest runs and thus your value gets updated. Wrap the update of the variable in $scope.apply() like so:
$scope.$apply(function(){
vm.alertCount = alertData.length;
});
This will force digest and update the value live.
If you have more values that are updated outside of digest (any callback, promise etc) you can force digest cycle by calling:
$scope.$apply();
Hope it helps.
EDIT -----
Given your update with full code, I see that you are not injecting scope anywhere in your controller, the controllers I write usually start like that:
(function () {
var app = angular.module('mainModule');
app.controller('myController', ['$scope', '$myService', function ($scope, $myService) {
//logic
}]);
}());
EDIT -----
Here is a quick go I had on your code:
(function() {
'use strict';
var app = angular.module('abp');
app.controller('MyLivestockController', ['$scope', 'userService', function($scope, userService) {
var vm = {};
$scope.vm = vm;
vm.myLivestockNotification = {
isLoading: true,
hasError: false
};
vm.alertsNotification = {
isLoading: true,
hasError: false,
hasData: false
};
vm.deleteAlert = function(id) {
vm.currentAlert = void 0;
vm.alertsNotification.isLoading = true;
userService.deleteAlert(vm.user.id, id).then(function() {
// Remove the alert from our Array
vm.alerts = vm.alerts.filter(function(alert) {
return alert.id !== id;
});
// Refresh the alert count for the user
vm.getAlerts(vm.user.id);
vm.alertsNotification.isLoading = false;
vm.alertsNotification.hasError = false;
}, function() {
vm.alertsNotification.hasError = true;
});
};
vm.getAlerts = function(id) {
userService.getAlerts(id).then(function(alertData) {
vm.alertCount = alertData.length;
if (vm.alertCount > 0) {
vm.alertsNotification.hasData = true;
} else {
vm.alertsNotification.hasData = false;
}
vm.alerts = alertData;
vm.alertsNotification.isLoading = false;
vm.alertsNotification.hasError = false;
//important, this is promise so we have to apply the scope to update view
$scope.$apply();
}, function() {
vm.alertsNotification.hasError = true;
});
};
// Init
(function() {
userService.getCurrentUser().then(function(data) {
vm.myLivestockNotification.hasError = false;
vm.myLivestockNotification.isLoading = false;
vm.user = data;
// Get alert count for the user
vm.getAlerts(vm.user.id);
}, function() {
vm.myLivestockNotification.hasError = true;
});
})();
}]);
})();
The general idea is:
you create an app (angular.module)
you create a controller in this app, with $scope injected
any values you want to be updated on your view, you add to $scope
if you have any $scope updates in a callback, event or promise, you wrap them in (or follow with) $scope.$apply call
I think this should work for you :)
I have attempted to reproduce your code below with a mock userService, and some slight modifications to the html view so we can more clearly see the alerts and delete them. I have not modified your Controller.
This appears to work, yes?
Which leads me to believe there may be some issue with the implementation of your userService. If you are able to post the relevant code, I can update this answer with a clarified solution.
UPDATE: As you've updated your question with the userService code, I've updated the below to more closely match. I still have a mock service standing in place of the user dependency of the userService. Additionally I made a couple of small edits to the Controller class so that while promises are still resolving we can see 'Updating...' in place of the alerts count.
This all still appears to work, unless I'm misunderstanding - will think on it more and update this 'answer' when I can think of where else to investigate for the source of the issue, see if we can at least reproduce it!
(function() {
'use strict';
function MyLivestockController(userService) {
var vm = this;
vm.myLivestockNotification = {
isLoading: true,
hasError: false
};
vm.alertsNotification = {
isLoading: true,
hasError: false,
hasData: false
};
vm.deleteAlert = function(id) {
vm.currentAlert = void 0;
vm.alertsNotification.isLoading = true;
return userService.deleteAlert(vm.user.id, id).then(function() {
// Remove the alert from our Array
vm.alerts = vm.alerts.filter(function(alert) {
return alert.id !== id;
});
// Refresh the alert count for the user
vm.getAlerts(vm.user.id).then(function() {
vm.alertsNotification.isLoading = false; //put here, loading isn't really finished until after .getAlerts() is done
vm.alertsNotification.hasError = false;
});
}, function() {
vm.alertsNotification.hasError = true;
});
};
vm.getAlerts = function(id) {
vm.alertsNotification.isLoading = true;
return userService.getAlerts(id).then(function(alertData) { //return the promise so we can chain .then in .deleteAlert()
vm.alertCount = alertData.length;
if (vm.alertCount > 0) {
vm.alertsNotification.hasData = true;
} else {
vm.alertsNotification.hasData = false;
}
vm.alerts = alertData;
vm.alertsNotification.isLoading = false;
vm.alertsNotification.hasError = false;
}, function() {
vm.alertsNotification.hasError = true;
});
};
// Init
(function() {
userService.getCurrentUser().then(function(data) {
vm.myLivestockNotification.hasError = false;
vm.myLivestockNotification.isLoading = false;
vm.user = data;
// Get alert count for the user
vm.getAlerts(vm.user.id);
}, function() {
vm.myLivestockNotification.hasError = true;
});
})();
}
function userMock($q, $timeout, $log) {
var _alerts = {
data: [{
id: 1,
message: "He doesn't sleep, he waits..."
}, {
id: 2,
message: "He doesn't mow his lawn, he stands outside and dares it to grow."
}, {
id: 3,
message: "Some magicians can walk on water. He can swim through land."
}]
},
_currentUser = {
id: 'Q2h1Y2sgTm9ycmlz'
};
return {
getCurrentUser: function getCurrentUser() {
$log.log("getCurrentUser");
//return $q.when(_currentUser);
return $timeout(function() { //use $timeout to simulate some REST API latency...
return _currentUser;
}, 500);
},
getAlerts: function getAlerts(id) {
$log.log("getAlerts: " + id); //not doing anything with the id in this mock...
$log.log(_alerts.data);
//return $q.when(_alerts);
return $timeout(function() {
return _alerts;
}, 500);
},
deleteAlert: function deleteAlert(userId, id) {
$log.log("deleteAlert: " + userId + " :: " + id);
//return $q.when(_alerts);
return $timeout(function() {
for (var i = 0; i < _alerts.data.length; i++) {
if (_alerts.data[i].id === id) {
_alerts.data.splice(i, 1);
$log.log("alert found and deleted");
break;
}
}
$log.log(_alerts.data);
return _alerts;
}, 500);
}
};
}
function userService($q, $timeout, $log, userMock) {
var service = this;
service.getCurrentUser = userMock.getCurrentUser;
service.getAlerts = function(id) {
var deferred = $q.defer();
userMock.getAlerts(id).then(function(response) {
if (response.hasOwnProperty('data')) {
// Convert 'he' to 'Chuck Norris'
angular.forEach(response.data, function(alert) {
if (alert.message) {
alert.message = alert.message.replace(/he/gi, "Chuck Norris");
}
});
deferred.resolve(response.data);
} else {
deferred.reject('DATA ERROR');
}
}, function(e) {
deferred.reject(e);
});
return deferred.promise;
};
service.deleteAlert = function(userId, id) {
var deferred = $q.defer();
userMock.deleteAlert(userId, id).then(function(response) {
deferred.resolve(response.data);
}, function(e) {
deferred.reject('DATA ERROR');
});
return deferred.promise;
};
return service;
};
angular
.module('abp', [])
.service('userMock', userMock)
.service('userService', userService)
.controller('MyLivestockController', MyLivestockController);
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="abp">
<div data-ng-controller="MyLivestockController as vm">
<div>Alerts</div>
<span data-ng-bind="vm.alertsNotification.isLoading ? 'Updating...' : vm.alertCount"></span>
<div data-ng-repeat="alert in vm.alerts">
{{alert.id}}: {{alert.message}}
<button ng-click="vm.deleteAlert(alert.id)">Delete</button>
</div>
</div>
</div>

Windows Azure + DevExrpess (PhoneJs) getting ToDoList (Standart Sample)

I'm starting to learn and azure phonejs.
Todo list get through a standard example:
$(function() {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
// Read current data and rebuild UI.
// If you plan to generate complex UIs like this, consider using a JavaScript templating library.
function refreshTodoItems() {
var query = todoItemTable.where({ complete: false });
query.read().then(function(todoItems) {
var listItems = $.map(todoItems, function(item) {
return $('<li>')
.attr('data-todoitem-id', item.id)
.append($('<button class="item-delete">Delete</button>'))
.append($('<input type="checkbox" class="item-complete">').prop('checked', item.complete))
.append($('<div>').append($('<input class="item-text">').val(item.text)));
});
$('#todo-items').empty().append(listItems).toggle(listItems.length > 0);
$('#summary').html('<strong>' + todoItems.length + '</strong> item(s)');
}, handleError);
}
function handleError(error) {
var text = error + (error.request ? ' - ' + error.request.status : '');
$('#errorlog').append($('<li>').text(text));
}
function getTodoItemId(formElement) {
return $(formElement).closest('li').attr('data-todoitem-id');
}
// Handle insert
$('#add-item').submit(function(evt) {
var textbox = $('#new-item-text'),
itemText = textbox.val();
if (itemText !== '') {
todoItemTable.insert({ text: itemText, complete: false }).then(refreshTodoItems, handleError);
}
textbox.val('').focus();
evt.preventDefault();
});
// Handle update
$(document.body).on('change', '.item-text', function() {
var newText = $(this).val();
todoItemTable.update({ id: getTodoItemId(this), text: newText }).then(null, handleError);
});
$(document.body).on('change', '.item-complete', function() {
var isComplete = $(this).prop('checked');
todoItemTable.update({ id: getTodoItemId(this), complete: isComplete }).then(refreshTodoItems, handleError);
});
// Handle delete
$(document.body).on('click', '.item-delete', function () {
todoItemTable.del({ id: getTodoItemId(this) }).then(refreshTodoItems, handleError);
});
// On initial load, start by fetching the current data
refreshTodoItems();
});
and it works!
Changed for the use of phonejs and the program stops working, even mistakes does not issue!
This my View:
<div data-options="dxView : { name: 'home', title: 'Home' } " >
<div class="home-view" data-options="dxContent : { targetPlaceholder: 'content' } " >
<button data-bind="click: incrementClickCounter">Click me</button>
<span data-bind="text: listData"></span>
<div data-bind="dxList:{
dataSource: listData,
itemTemplate:'toDoItemTemplate'}">
<div data-options="dxTemplate:{ name:'toDoItemTemplate' }">
<div style="float:left; width:100%;">
<h1 data-bind="text: name"></h1>
</div>
</div>
</div>
</div>
This my ViewModel:
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = ko.observableArray([
{ name: "111", type: "111" },
{ name: "222", type: "222" }]);
var query = todoItemTable.where({ complete: false });
query.read().then(function (todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
});
var viewModel = {
listData: toDoArray,
incrementClickCounter: function () {
todoItemTable = client.getTable('todoitem');
toDoArray.push({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
};
I can easily add items to the list of programs, but from the server list does not come:-(
I am driven to exhaustion and can not solve the problem for 3 days, which is critical for me!
Specify where my mistake! Thank U!
I suggest you use a DevExpress.data.DataSource and a DevExpress.data.CustomStore instead of ko.observableArray.
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = [];
var store = new DevExpress.data.CustomStore({
load: function(loadOptions) {
var d = $.Deferred();
if(toDoArray.length) {
d.resolve(toDoArray);
} else {
todoItemTable
.where({ complete: false })
.read()
.then(function(todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
d.resolve(toDoArray);
});
}
return d.promise();
},
insert: function(values) {
return toDoArray.push(values) - 1;
},
remove: function(key) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray.splice(key, 1);
},
update: function(key, values) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray[key] = $.extend(true, toDoArray[key], values);
}
});
var source = new DevExpress.data.DataSource(store);
// older version
store.modified.add(function() { source.load(); });
// starting from 14.2:
// store.on("modified", function() { source.load(); });
var viewModel = {
listData: source,
incrementClickCounter: function () {
store.insert({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
}
You can read more about it here and here.

backbone, insert model while maintaining sort order

When a new model is added (via "set" function of the collection), I want the model be inserted at the index maintaining sort order, instead at the end.
Thanks
var Ts = (function () {
var Result = Backbone.Model.extend({
idAttribute : 'PAG_ID'
});
var ResultList = Backbone.Collection.extend({
model: Result,
comparator: function(result) {
//console.log(result);
return result.get('SORT_KEY');
},
});
var resultsCollection = new ResultList(data);
data = undefined;
var TableView = Backbone.View.extend({
tagName: 'table',
initialize : function() {
_.bindAll(this, 'render', 'renderRow');
this.collection.on("add", this.renderRow, this);
},
render: function() {
$(this.el).attr('id', 'tsTable').addClass('resulttable');
this.renderHeader(this.collection.shift());
this.collection.each(this.renderRow);
return this;
},
renderHeader : function(model) {
var col=new HeaderView({model:model});
this.$el.append(col.render().$el);
return this;
},
renderRow : function(model) {
var row=new RowView({model:model});
this.$el.append(row.render().$el);
return this;
}
});
var HeaderView = Backbone.View.extend({
tagName: 'tr',
model: resultsCollection.models,
initialize: function() {
this.model.on('change',this.render,this);
},
render: function() {
var html=_.template(colTemplate,this.model.toJSON());
this.$el.html(html);
return this;
}
});
var RowView = Backbone.View.extend({
tagName: 'tr',
initialize: function() {
this.model.on('all',this.render,this);
},
remove: function () {
debug.log("Called remove event on model");
$(this.el).remove();
},
model: resultsCollection.models,
render: function() {
var html=_.template(rowTemplate,this.model.toJSON());
this.$el.html(html);
return this;
},
attributes : function () {
return {
id : this.model.get('PAG_ID')
};
}
});
var tableView = new TableView({collection: resultsCollection});
$("body").append( tableView.render().$el );
resultsCollection.set(initialdata);
resultsCollection.set(someotherdata, {merge: true});
I have changed to as below and it works.Not sure if this is the best implementation
renderRow : function(model) {
var row = new RowView({model:model});
var index = model.get('SORT_KEY') - 1;
row.render().$el.insertAfter(this.$el.find('tr:eq('+ index +')'));
return this;
}
If you provide a comparator function on your collection, Collection.set will perform a silent sort after the new models have been spliced in.
From backbones source http://backbonejs.org/docs/backbone.html:
set: function(models, options) {
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
...
if (toAdd.length) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
splice.apply(this.models, [at, 0].concat(toAdd));
} else {
push.apply(this.models, toAdd);
}
}
if (sort) this.sort({silent: true});
Here is a fiddle demonstrating that collection.set respects a comparator.
http://jsfiddle.net/puleos/sczV3/

Backbone object fields are from previous item

I've just started using Backbone.js and my test cases are churning up something pretty weird.
In short, what I am experiencing is -- after I call a Backbone Model's constructor, some of the fields in my object seem to come from a previously item. For instance, if I call:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id
});
playlist.get('items').length; //1
however if I do:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id,
items: []
});
playlist.get('items').length; //0
Here's the code:
define(['ytHelper', 'songManager', 'playlistItem'], function (ytHelper, songManager, PlaylistItem) {
'use strict';
var Playlist = Backbone.Model.extend({
defaults: {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
},
initialize: function () {
//Our playlistItem data was fetched from the server with the playlist. Need to convert the collection to Backbone Model entities.
if (this.get('items').length > 0) {
console.log("Initializing a Playlist object with an item count of:", this.get('items').length);
console.log("items[0]", this.get('items')[0]);
this.set('items', _.map(this.get('items'), function (playlistItemData) {
var returnValue;
//This is a bit more robust. If any items in our playlist weren't Backbone.Models (could be loaded from server data), auto-convert during init.
if (playlistItemData instanceof Backbone.Model) {
returnValue = playlistItemData;
} else {
returnValue = new PlaylistItem(playlistItemData);
}
return returnValue;
}));
//Playlists will remember their length via localStorage w/ their ID.
var savedItemPosition = JSON.parse(localStorage.getItem(this.get('id') + '_selectedItemPosition'));
this.selectItemByPosition(savedItemPosition != null ? parseInt(savedItemPosition) : 0);
var songIds = _.map(this.get('items'), function(item) {
return item.get('songId');
});
songManager.loadSongs(songIds);
this.set('shuffledItems', _.shuffle(this.get('items')));
}
},
//TODO: Reimplemnt using Backbone.sync w/ CRUD operations on backend.
save: function(callback) {
if (this.get('items').length > 0) {
var selectedItem = this.getSelectedItem();
localStorage.setItem(this.get('id') + '_selectedItemPosition', selectedItem.get('position'));
}
var self = this;
console.log("Calling save with:", self);
console.log("my position is:", self.get('position'));
$.ajax({
url: 'http://localhost:61975/Playlist/SavePlaylist',
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(self),
success: function (data) {
console.log('Saving playlist was successful.', data);
self.set('id', data.id);
if (callback) {
callback();
}
},
error: function (error) {
console.error("Saving playlist was unsuccessful", error);
}
});
},
selectItemByPosition: function(position) {
//Deselect the currently selected item, then select the new item to have selected.
var currentlySelected = this.getSelectedItem();
//currentlySelected is not defined for a brand new playlist since we have no items yet selected.
if (currentlySelected != null && currentlySelected.position != position) {
currentlySelected.set('selected', false);
}
var item = this.getItemByPosition(position);
if (item != null && item.position != position) {
item.set('selected', true);
localStorage.setItem(this.get('id') + '_selectedItemPosition', item.get('position'));
}
return item;
},
getItemByPosition: function (position) {
return _.find(this.get('items'), function(item) {
return item.get('position') == position;
});
},
addItem: function (song, selected) {
console.log("this:", this.get('title'));
var playlistId = this.get('id');
var itemCount = this.get('items').length;
var playlistItem = new PlaylistItem({
playlistId: playlistId,
position: itemCount,
videoId: song.videoId,
title: song.title,
relatedVideos: [],
selected: selected || false
});
this.get('items').push(playlistItem);
this.get('shuffledItems').push(playlistItem);
this.set('shuffledItems', _.shuffle(this.get('shuffledItems')));
console.log("this has finished calling");
//Call save to give it an ID from the server before adding to playlist.
songManager.saveSong(song, function (savedSong) {
song.id = savedSong.id;
playlistItem.set('songId', song.id);
console.log("calling save item");
$.ajax({
type: 'POST',
url: 'http://localhost:61975/Playlist/SaveItem',
dataType: 'json',
data: {
id: playlistItem.get('id'),
playlistId: playlistItem.get('playlistId'),
position: playlistItem.get('position'),
songId: playlistItem.get('songId'),
title: playlistItem.get('title'),
videoId: playlistItem.get('videoId')
},
success: function (data) {
playlistItem.set('id', data.id);
},
error: function (error) {
console.error(error);
}
});
});
return playlistItem;
},
addItemByVideoId: function (videoId, callback) {
var self = this;
ytHelper.getVideoInformation(videoId, function (videoInformation) {
var song = songManager.createSong(videoInformation, self.get('id'));
var addedItem = self.addItem(song);
if (callback) {
callback(addedItem);
}
});
},
//Returns the currently selected playlistItem or null if no item was found.
getSelectedItem: function() {
var selectedItem = _.find(this.get('items'), function (item) {
return item.get('selected');
});
return selectedItem;
}
});
return function (config) {
var playlist = new Playlist(config);
playlist.on('change:title', function () {
this.save();
});
return playlist;
};
});
basically I am seeing the property 'items' is populated inside of initialize when I've passed in a config object that does not specify items at all. If I specify a blank items array in my config object, then there are no items in initialize, but this seems counter-intuitive. Am I doing something wrong?
The problem is with using reference types (arrays) in the defaults object. When a new Playlist model is created without specifying an items value, the default is applied. In case of arrays and objects this is problematic, because essentially what happens is:
newModel.items = defaults.items
And so all models initialized this way refer to the same array. To verify this, you can test:
var a = new Playlist();
var b = new Playlist();
var c = new Playlist({items:[]});
//add an item to a
a.get('items').push('over the rainbow');
console.log(b.get('items')); // -> ['over the rainbow'];
console.log(c.get('items')); // -> []
To get around this problem, Backbone supports defining Model.defaults as a function:
var Playlist = Backbone.Model.extend({
defaults: function() {
return {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
};
}
});

Categories