In Backbone how can I remove models from a collection based on a filter/query? - backbone.js

I am using Backbonejs 0.9.2 with Titanium Alloy and I need to remove all the completed tasks from a Collection. Backbone.sync is configured to use a SQLite local database.
extendCollection: function(Collection) {
exts = {
self: this
, fetchComplete: function() {
var table = definition.config.adapter.collection_name
this.fetch({query:'SELECT * from ' + table + ' where complete=1'})
}
, removeComplete: function () {
this.remove(this.fetchComplete())
}
}
_.extend(Collection.prototype, exts);
return Collection
}
My Jasmine tests look like this
describe("task model", function () {
var Alloy = require("alloy"),
data = {
taskId: 77
},
collection,
item;
beforeEach(function(){
collection = Alloy.createCollection('task');
item = Alloy.createModel('task');
});
// PASSES
it('can fetch complete tasks', function(){
item.set(data);
item.save();
collection.fetchComplete();
expect(0).toEqual(collection.length);
item.markAsComplete();
item.save();
collection.fetchComplete();
expect(1).toEqual(collection.length);
});
// FAILS
it('can remove completed tasks', function(){
// we have 6 items
collection.fetch()
expect(6).toEqual(collection.length);
// there are no completed items
collection.fetchComplete();
expect(0).toEqual(collection.length);
item.set(data);
item.save();
item.markAsComplete();
item.save();
// we have 7 items 1 of which is complete
collection.fetch()
expect(7).toEqual(collection.length);
collection.removeComplete()
// after removing the complete item we should have 6 left
collection.fetch()
expect(6).toEqual(collection.length);
});
afterEach(function () {
item.destroy();
});
});

Iterate through your collection or use some of the helper functions via underscore and then call the remove function and pass in your model. See the docs here: http://backbonejs.org/#Collection-remove

Alternatively you can use underscores' _.filter method::
_.filter(tasks, function (task) {
return task.status == 'ACTIVE'
});
See this fiddle:
http://jsfiddle.net/Y3gPJ/

Related

2 way binding issues with directives, controllers and services

This is bugging me a bit.
I have a service that handles logo panels and a function that is used to navigate between the different panels.
When getPanels is invoked it sets the currentPanel, index and length on the service when all promises have completed (see $q.all in the getPanels method):
.service('ConfiguratorLogoService', ['$q', 'UploadService', 'LogoService', 'ArrayService', 'SvgService', function ($q, uploadService, logoService, arrayService, helper) {
// Private function to build a file array
var _buildFileArray = function (panels, files) {
//--- Omitted for brevity ---//
};
// Create our service
var service = {
// Create our arrays
panels: [],
files: [],
currentPanel: null,
index: 0,
length: 0,
// Get our panels
getPanels: function (container, garmentId) {
// Create a deferred promise
var deferred = $q.defer();
// Create our arrays
var panels = []
files = [],
promises = [];
// If we have a container
if (container) {
// Get the containers children
var children = container.children()
// Loop through our panel's children
for (var i = 0; i < children.length; i++) {
// Get the current child
var child = angular.element(children[i]),
childId = child.attr('id'),
childTitle = helper.extractText(childId, ':', 1);
// Create our item
var panel = {
id: childId,
title: childTitle
};
// Try to get our item
promises.push(logoService.get(garmentId, panel.id).then(function (response) {
// If we have any data
if (response) {
// Add the file to our array
files.push(response);
}
}));
// Add our child to the array
panels.push(panel);
}
}
// After all the promises have been handled
$q.all(promises).then(function () {
// Get our files
service.files = _buildFileArray(panels, files);
service.panels = panels;
service.currentPanel = panels[0];
service.length = panels.length;
// Resolve our promise
deferred.resolve({
files: service.files,
panels: panels
});
});
// Return our promise
return deferred.promise;
},
// Get our next panel
navigateNext: function () {
// Create a deferred promise
var deferred = $q.defer();
// Get the next index or reset if we reached the end of our list
service.index = service.index === (service.length - 1) ? 0 : service.index += 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
console.log(service.index);
// Resolve our promise
deferred.resolve();
// Return our promise
return deferred.promise;
},
// Get our previous panel
navigatePrevious: function () {
// Get the previous index or set to the end of our list
service.index = service.index === 0 ? service.length - 1 : service.index -= 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
},
// Removes the file from azure
remove: function (index) {
//--- Omitted for brevity ---//
}
};
// Return our service
return service;
}])
which is fine, it works and the first panel is selected.
So, I have a controller, which is attached to a directive. The controller looks like this:
.controller('ConfiguratorLogosDirectiveController', ['ConfiguratorLogoService', 'RowService', function (service, rowService) {
var self = this;
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
// Initialization
self.init = function (container, garmentId) {
// Get our panels
return service.getPanels(container, garmentId).then(function (response) {
self.panels = response.panels;
self.files = response.files;
// If we have any panels
if (self.panels.length) {
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
}
// Return our response
return response;
})
};
// Map our service functions
self.upload = service.upload;
self.next = service.navigateNext;
self.previous = service.navigatePrevious;
self.remove = service.remove;
}])
As you can see, when I get my panels, I set the currentPanel, index and length on the controller itself which I didn't think I would have to do because when the controller is invoked, it already has a reference to the service values. I figured 2 way binding would come into play and when the service values update, the controller would update too.
Anyway, I update the values after the getPanels method completes successfully. In my directive I have this:
// Invoke on controller load
controller.init(container, scope.garmentId).then(function (response) {
// Map our properties
scope.panels = controller.panels;
scope.files = controller.files;
scope.currentPanel = controller.currentPanel;
scope.index = controller.index;
scope.length = controller.length;
});
which again works fine. In my template I can see the first panel and it looks fine.
So, then came the next step which was my navigate functions. I started with next which I have modified for testing purposes so I can output the controller.index as well as the console.log in the service navigation function.
The directive function looks like this:
scope.next = function () {
controller.next().then(function () {
console.log(controller.index);
});
};
When this method is invoked, I can see in my console that the service increases the index by 1 but the controller still shows 0 which means that 2 way binding is not working.
I am about to update my method in the controller to push the currentPanel and index to the controller, but before I do I thought I would ask here first.
So, does anyone know why my 2 way binding isn't working?
So my current workaround works, but I just don't like it.
In my directive I have done this:
scope.next = function () {
controller.next().then(function () {
console.log(controller.index);
scope.currentPanel = controller.currentPanel;
scope.index = controller.index;
scope.length = controller.length;
});
}
and in the directive controller I have done this:
self.next = function () {
// Try to navigate forward
return service.navigateNext().then(function () {
// Set our current panel
self.currentPanel = service.currentPanel;
self.index = service.index;
self.length = service.length;
console.log(self.index);
});
}
and in my service, it looks the same as before:
// Get our next panel
navigateNext: function () {
// Create a deferred promise
var deferred = $q.defer();
// Get the next index or reset if we reached the end of our list
service.index = service.index === (service.length - 1) ? 0 : service.index += 1;
// Set our active panel
service.currentPanel = service.panels[service.index];
console.log(service.index);
// Resolve our promise
deferred.resolve();
// Return our promise
return deferred.promise;
},
This works, but surely this is not the way it should work.
I have figured it out thanks to this article.
I just had to create an object in my directive and bind the values to that.
Doing that fixed the issues.

Backbone.js - model.save() not firing a PUT request

I have a basic application using Backbone.js that is not making PUT calls (updating model). From the front-end, I calling a models save function doesn't make a PUT call; however, if I replace it with destroy, it does make a DELETE call to the back-end. Anyone have any idea what might be the issue? The function that is not firing a PUT request is the saveTask function.
App.Views.Task = Backbone.View.extend({
template: _.template("<label>ID:</label><input type='text' id='taskId' name='id' value='<%= _id %>' disabled /><br><label>Title:</label><input type='text' id='title' name='title' value='<%= title %>' required/><br><label>Content:</label><input type='text' id='content' name='content' value='<%= content %>'/><br><button class='save'>Save</button>"),
events: {
"change input":"change",
"click .save":"saveTask"
},
render: function(eventName){
$(this.el).html(this.template(this.model.toJSON()));
//console.log(this.generateTemplate());
return this;
},
change: function(event){
var target = event.target;
console.log('changing ' + target.id + ' from: ' + target.defaultValue + ' to: ' + target.value);
change[target.name] = target.value;
this.model.set(change);*/
},
saveTask: function(){
this.model.set({
title:$("#title").val(),
content:$("#content").val()
});
if(this.model.isNew()){
App.taskList.create(this.model);
} else {
this.model.save({});
}
}
});
If your model is new, then at the time you save it it will fire a post method.
If your model however is not new and you are updating it, it will fire a PUT.
if this is not working for you it may be because your model does not have an id property, in case you are using an id with a different name, for example taskID, then in your model you have to set the idAttribute to taskID so backbone uses this property as the Id and everything will be normal.
like this:
var Task= Backbone.Model.extend({
idAttribute: "taskId"
});
here is the link to the documentation on Idattibute
http://backbonejs.org/#Model-idAttribute
also another problem could be the {} in your save call
try just
this.model.save();
instead of
this.model.save({});
I believe model is always expecting options parameter and also probably the callbacks
this.model.save(null, {
success: function (model, response) {
//
},
error: function () {
//
}
});
If you look at Backbone src, you will notice that too...
======
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function (key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({
validate: true
}, options);
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false;
}
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function (resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
In my case it fails due to validations.As i save the model it validates all the attributes of the model and the collection which i am using for listing interface doesn't required all the attributes of the model.
I was facing the same issues and search in Google and found your question and read the solution and comments.Than i realize that in updated backbone specifications it is mentioned that when model.save() executes before model requests,it first call validate and if validate succeeds than it will go ahead other wise fails, and that is the reason why it doesn't showing any network request in chrome debugger network tab.
I have write the solution for the case which i am facing,other might be facing different issues.
Backbone's sync function is what I wound up using. You have to pass in 'update' as the first parameter (the 'method' parameter).

How to make selected item to change class(How to make property observable in backbone)

i want to build simple screen, where list of items in one side and selected item details -on another. when user click on one of the items - its details displayed in 'details' section.
also the selected item in the 'list' section must be decorated with 'active' class.
here is my router code:
var AppRouter = Backbone.Router.extend({
routes:{
"":"list",
"users/:id":"userDetails"
},
list:function () {
this.usersList = new UsersCollection(usersList);/* new UsersCollection()*/
var self = this;
//this.userList.fetch({
// success: function () {
this.UsersListView = new UsersListView({ model: this.usersList });
$('#sidebar').html(this.UsersListView.render().el);
// }
//})//end of fetch
},
userDetails:function (id) {
if(this.usersList){
//unselect prevously selected
if(this.user )this.user.set({'selected':false});
this.user = this.usersList.get(id);
//select current
this.user.set({'selected':true});
//empty refill the items section
this.UsersListView = new UsersListView({ model: this.usersList });
$('#sidebar').empty().html(this.UsersListView.render().el);
if (this.UserDetailsView) this.UserDetailsView.close();
this.UserDetailsView = new UserDetailsView({model:this.user});
$('#content').html(this.UserDetailsView.render().el);
}
else{
}
}
});
So far i managed to set the 'active' item class by emptying and refill the items section html.
Is there any way to observe (like in knockoutjs) the 'selected' property, so once it changes -the change will be visible in html?
code of view:
window.UserListItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#tpl-user-list-item').html()),
render:function (eventName) {
if(this.model.get('selected')){$(this.el).addClass('active');}
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
Thanks forwards
This is what you're looking for (especially the Events#listenTo method).
So; in your view:
initialize: function() {
// listen to your model
this.listenTo(this.model, 'change:selected', this.updateClass);
},
updateClass: function() {
// something like...
this.$el.toggleClass('active');
}

passing data to a collection in backbone

So I am trying storing product types from a json file before trying to add them to a collection but am getting some strange results (as in I dont fully understand)
on my router page i setup a variable for cached products as well as product types
cachedProductTypes: null,
productType : {},
products : {},
getProductTypes:
function(callback)
{
if (this.cachedProductTypes !== null) {
return callback(cachedProductTypes);
}
var self = this;
$.getJSON('data/product.json',
function(data)
{
self.cachedProductTypes = data;
callback(data);
}
);
},
parseResponse : function(data) {
result = { prodTypes: [], products: [] };
var type;
var types = data.data.productTypeList;
var product;
var i = types.length;
while (type = types[--i]) {
result.prodTypes.push({
id: type.id,
name: type.name,
longName: type.longName
// etc.
});
while (product = type.productList.pop()) {
product.productTypeId = type.id,
result.products.push(product);
}
};
this.productType = result.prodTypes;
console.log( "dan");
this.products = result.products;
},
showProductTypes:function(){
var self = this;
this.getProductTypes(
function(data)
{
self.parseResponse(data);
var productTypesArray = self.productType;
var productList=new ProductsType(productTypesArray);
var productListView=new ProductListView({collection:productList});
productListView.bind('renderCompleted:ProductsType',self.changePage,self);
productListView.update();
}
);
}
when a user goes to the show product types page it runs the showProductsType function
So I am passing the products type array to my collection
on the collection page
var ProductsType=Backbone.Collection.extend({
model:ProductType,
fetch:function(){
var self=this;
var tmpItem;
//fetch the data using ajax
$.each(this.productTypesArray, function(i,prodType){
tmpItem=new ProductType({id:prodType.id, name:prodType.name, longName:prodType.longName});
console.log(prodType.name);
self.add(tmpItem);
});
self.trigger("fetchCompleted:ProductsType");
}
});
return ProductsType;
now this doesnt work as it this.productTypesArray is undefined if i console.log it.
(how am I supposed to get this?)
I would have thought I need to go through and add each new ProductType.
the strange bit - if I just have the code
var ProductsType=Backbone.Collection.extend({
model:ProductType,
fetch:function(){
var self=this;
var tmpItem;
//fetch the data using ajax
self.trigger("fetchCompleted:ProductsType");
}
});
return ProductsType;
it actually adds the products to the collection? I guess this means I can just pass an array to the collection and do not have to add each productType?
I guess this means I can just pass an array to the collection and do not have to add each productType?
Yes, you can pass an array to the collection's constructor, and it will create the models for you.
As far as your caching code, it looks like the problem is here:
if (this.cachedProductTypes !== null) {
return callback(cachedProductTypes);
}
The callback statement's argument is missing this - should be return callback(this.cachedProductTypes).

how can I update a model with custom idAttribute

in my simple backbone application, I am trying to update a model and every time it send a put request instead of post.
Well, this is my model named categoryModel
define(['Backbone'], function (Backbone) {
var CategoryModel = Backbone.Model.extend({
defaults: {
ID: '',
Name: 'Empty',
TagID: '0',
GID: '0'
},
idAttribute: "ID",
initialize: function () {
if (!this.get('Name')) {
this.set({ 'Name': this.defaults.Name });
}
}
});
return CategoryModel;
});
this is the collection
define(['Backbone','../../models/categories/categoryModel'], function (Backbone, categoryModel) {
var CategoryCollection = Backbone.Collection.extend({
url: '/parentcategory/Actions',
model: categoryModel
});
return new CategoryCollection;
});
here are my methods in the view
on a keychange event
createNewItem: function (e) {
var $this = $(e.currentTarget);
$('#selectedCategoryName').html($this.val());
//it creates a new model
globals.NewCategory = new CategoryModel({ Name: $this.val() });
}
on handleDrop event
handleDropEvent: function (event, ui) {
var draggable = ui.draggable;
//check if name has set
if (!globals.NewCategory) {
alert("Please write a category name");
$('#createNewCategory').focus();
return;
}
//get itemID
var itemID = draggable.attr("id").split('_')[1];
var itemDesc = draggable.attr("id").split('_')[0];
//check items category
if (itemDesc == "Tag") {
//check if tagID already exists
if (globals.NewCategory.TagID) {
alert("you have already specify a tag from this category");
return;
}
globals.NewCategory.set("TagID", itemID);
} else if (itemDesc == "gTag") {
if (globals.NewCategory.GID) {
alert("you have already specify a tag from this category");
return;
}
globals.NewCategory.set("GID", itemID);
}
categoriesCollection.create(globals.NewCategory, {
silent: true,
wait: true,
success: function (model, response) {
model.set("ID", response);
alert(model.id);
}
});
}
The categoriesCollection.create is called twice. Firstly for setting the TagID (on a success request it gets an ID ) and secondly for setting the GID.
Since the ID has been set, shouldn't had sent a POST request instead of PUT on the second call?
What am I doing wrong?
Thanks
The standard behaviour is to send a POST if the model is new ( doesn't have an ID attributed ) and send a PUT if the model id is set.
In your case it's working as designed, if you want it to use POST to send UPDATES you have to override Backbone.sync to work as you need, but I think it's easier for you to make your backend RESTful and create a PUT listener controller method for updates.
Another thing, if I got it right you are using create() to update models in your collection, I would advise you not to do that and instead use the save() directly in the model you want to update, the code will be a lot more readable.
Cheers.

Resources