This is my Backbone collection:
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: Todo,
url: function () {
return 'api/todos';
},
// Filter down the list of all todo items that are finished.
done: function () {
return this.filter(function (todo) { return todo.get('done'); });
},
// Filter down the list to only todo items that are still not finished.
remaining: function () {
return this.without.apply(this, this.done());
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function () {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function (todo) {
return todo.get('order');
},
addToDo: function (opts) {
var model = this;
opts.url = model.url() + '/AddToDo'
// add any additional options, e.g. a "success" callback or data
_.extend(options, opts);
return (this.sync || Backbone.sync).call(this, null, this, options);
}
});
This works fine and I can hit my URL endpoint. However, the issue is with the built in create defined in Backbone collection, the model gets automatically added to the collection once create is called. With my custom method addToDo the to-do is added successfully but I can't see it in my view unless I refresh the page.
What am I missing? Any help is appreciated!
.create is syntactic sugar around model.save and collection.add
If you're model is being created in a different way, you need to override your sync method on the model to something like:
sync: function (method, model, options) {
options || (options = {});
if (method === 'create') {
options.url = _.result(model, 'url') + '/AddToDo';
}
return Backbone.Model.prototype.sync.call(this, method, model, options);
}
Related
I have an object of products. The products i get from a resource (Product), store in a factory (productsStore), and iterate through in a controller.
In another controller, i want to either empty of refresh products via productsStore.empty(); or productsStore.get();. But either of them does nothing. Meaning my forEach, below, is not being run again, and the list not updated.
What am i doing wrong?
Controller:
vm.products = productsStore.get();
vm.products.$promise.then(function (data) {
angular.forEach(vm.products, function (child) {
//. some code
)};
)};
Factory:
myApp.factory('productsStore', function ($http, $q, Product) {
var products = "";
var get = function () {
return products = Product.query();
};
var empty = function () {
return products = {};
};
// Bind products
products = get();
return {
get: get,
empty: empty
};
});
Resource:
myApp.factory('Product', function ($resource) {
return $resource('http://api.com/api/products/:id', { id: '#id' }, {
update: {
method: 'PUT'
}
});
});
try to loop through the data you get after the promise is resolved, otherwise it's a rather useless variable.
angular.forEach(data, function(child) {
//...do something with the child
});
Also, you have a typo, that I'm not sure you have in your actual code. In your controller, end of block should be }) and not )}
var Order = Backbone.Model.extend({
url: function() {
return 'http://localhost:51782/api/orders/'+this.id;
}
});
var DataSetOrders = Backbone.Collection.extend({
url: "http://localhost:51782/api/orders",
model: Order,
initialize: function(){
this.fetch({
success: this.fetchSuccess,
error: this.fetchError
});
},
// This is where my data is being extracted I am returning the
// response since its an array of all the objects within my database
// perhaps i should not use return? Is there a way to save this information so it can always
// be available when i call DataSetOrders();?
fetchSuccess: function (collection, response) {
// console.log('Collection fetch success', response);
// console.log('Collection models: ', collection.models);
return response;
},
fetchError: function (collection, response) {
throw new Error("Orders fetch error");
}
});
var DataSetOrdersView = Backbone.View.extend({
el: $("#orders"),
// collection: new DataSetOrders(),
initialize: function () {
this.collection = new DataSetOrders();
this._modelBinder = new Backbone.ModelBinder();
this.render();
this.$el.find("#filter").append(this.createSelect());
this.on("change:filterType", this.filterByStatus, this);
this.collection.on("reset", this.render, this);
this.collection.on("add", this.renderOrder, this);
this.collection.on("remove", this.removeOrder, this);
},
render: function () {
this.$el.find("article").remove();
// This is the line where we are getting all of our models and rendering it to the #orders view
// as you can see it is referencing the this.collection = new DataSetOrders()
// This is actually working the view is being rendered and I can see the orders being pulled from
// the database
_.each(this.collection.models, function (item) {
this.renderOrder(item);
}, this);
},
renderOrder: function (item) {
// The item which is an element of the previous array returned is now being placed in the model
// and its being rendered prefectly.
var orderView = new OrderView({
model: item
});
this.$el.append(orderView.render().el);
// this.$el.append(this._modelBinder.bind(orderView.render().el));
},
getTypes: function () {
// HERE!!! IS where this.collection is now = [] wtf?!? I dont know why its doing this pleace help me!!
return _.uniq(this.collection.pluck("OrderStatus"), false, function (type) {
return type.toLowerCase();
});
},
Guys sorry for the long code, but I have a simple question. Why does the first time my code calls when using the method render this.collection has the elements within it and because of my code structure it renders my database objects and works perfectly. But when I call this.collection again on my geTypes function I get an empty array I dont understand whats going on??
I've been learning ember by recreating the TodoMVC in EmberCli. Ive recreated all the functionality, but I ran into an issue and I was hoping someone could shed some light on the situation.
It seems that my Todos ArrayController will observe and fire functions when properties in my model change but not when values in my Todo ObjectController change.
I moved isEditing into the Model so that when I call editTodo canToggle fires. But I would prefer to store that value in my controller and not the model.
I set up a test with with a propTest boolean. on a button click I fire propTogglebut todoPropToggle doesn't respond to the change. The only time that it does ever fire is on initialization.
Any insight would be super helpful.
TODOS CONTROLLER
import Ember from 'ember';
export default Ember.ArrayController.extend({
actions: {
createTodo: function() {
var title = this.get('newTitle');
if (!title.trim()) {
return;
}
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false,
isEditing:false
});
this.set('newTitle', '');
todo.save();
}
},
canToggle: function() {
var isEditing = this.isAny('isEditing');
return this.get('length') && !isEditing;
}.property('length','#each.isEditing'),
todoPropToggle: function() {
var hasPropTest = this.isAny('propTest');
return hasPropTest;
}.property('#each.propTest')
});
TODO CONTROLLER
import Ember from 'ember';
export default Ember.ObjectController.extend({
actions: {
editTodo: function() {
var todo = this.get('model');
todo.set('isEditing', true);
},
removeTodo: function() {
var todo = this.get('model');
todo.deleteRecord();
todo.save();
},
acceptChanges: function() {
var todo = this.get('model');
todo.set('isEditing', false);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
}
else {
this.get('model').save();
}
},
propToggle:function(){
this.set('propTest',!this.get('propTest'));
}
},
propTest:true,
isCompleted: function(key, value) {
var model = this.get('model');
if (value === undefined) {
return model.get('isCompleted');
}
else {
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
How about an alternative approach? We could toggle 'canToggle' in arrayController directly from the object controller by either using parentController or specifying needs. Avoids having to observer all the itemControllers which should be more efficient too.
TODOS CONTROLLER:
import Ember from 'ember';
export default
Ember.ArrayController.extend({
/**
* references the todo model that is currently being edited
*/
editingTodo: null,
canToggle: Ember.computed.notEmpty('editingTodo'),
actions: {
createTodo: function () {
var title = this.get('newTitle');
if (!title.trim()) {
return;
}
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false,
isEditing: false
});
this.set('newTitle', '');
todo.save();
}
}
});
TODO CONTROLLER
import Ember from 'ember';
export default
Ember.ObjectController.extend({
needs:['todos']
todos : Ember.computed.alias('controllers.todos'),
actions: {
editTodo: function () {
this.set('todos.editingTodo', this.get('model'));
},
removeTodo: function () {
var todo = this.get('model');
if (this.get('todos.editingTodo') === todo) {
this.set('todos.editingTodo', null);
}
todo.deleteRecord();
todo.save();
},
acceptChanges: function () {
this.set('todos.editingTodo', null);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
}
else {
this.get('model').save();
}
}
},
isCompleted: function (key, value) {
var model = this.get('model');
if (value === undefined) {
return model.get('isCompleted');
}
else {
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
I have not tested it, but I hope you get the jist of the solution I am proposing.
propTest in this scenario here looks like it's a computed property, and not an observable. (edit: not that computed properties don't have underlying observables powering them, but they're different in usage enough that I like to keep them separate) It'll fire when the underlying propTest fires, but if there's no change, ember will no-op that out in the run loop. If you'd rather this be a raw observable, use the observes() syntax. #each will work here, but I like being explicit and have an observable update what it needs to rather than using the computed property, unless I need direct access to that property to bind with in a template.
The "only firing on execution" stems from the initial bindings from the computed property getting created. If it never fires after that, the underlying bindings you're using for the computed property #each.propTest must be incorrect, otherwise that would indeed fire.
I think you may also be confused to the purpose of an ObjectController. An Ember.Object can do all of the observable business that a controller can, short of having a 'model' or 'content' property backing it. It looks like you may want to go with a straight up object rather than a controller here for the todo, since ember doesn't really have a 'model' type. I'd then put the objects themselves as part of the content of the ArrayController, at which point #each would be able to iterate on them as you'd expect.
The ObjectController sits at the same level of usage as an ArrayController. You can certainly nest them as you've done here, but my spidey-sense is tingling that it's the wrong thing to do given the application. You probably don't need to have a backing controller for each todo object, you just need the todo object itself.
I want to create a find method that loops through an array returned by the $resource service in Angular.
If I have a service like so:
'use strict';
angular.module('adminApp').factory('ProductType', function($resource) {
var ProductType;
ProductType = $resource('http://localhost:3000/api/v1/product_types/:id.json', {
id: '#id'
}, {
update: {
method: 'PUT'
}
});
ProductType.find = function(typeName){
var types = this.query(),
typeObject = {},
self = this;
for(type in types) {
var result = types[type],
resultName = self.normalizeName(result.name),
if(typeName === resultName) {
typeObject = result;
}
}
return typeObject;
};
return ProductType;
});
I tried wrapping it all in a function and returning the function thinking it had something to do with it being async and I also tried nesting a callback in the query method but that just allowed me to modify the response and not actually return anything differently.
When I try and set the return value to $scope in the controller I get a blank object
The this.query() method would return an array which might not be filled until the this.query() method has got its results back from the server. You will need to do something like this to wait until the call to the server has completed. As this is sort of async you will need to return a promise from this method that is resolved when the initial query has completed and you have searched the results.
'use strict';
angular.module('adminApp').factory('ProductType', [
'$q',
'$resource',
function($q, $resource) {
var ProductType;
ProductType = $resource('http://localhost:3000/api/v1/product_types/:id.json', {
id: '#id'
}, {
update: {
method: 'PUT'
}
});
ProductType.find = function(typeName) {
var defer = $q.defer(),
types = this.query(),
self = this;
types.$promise.then(function () {
var result,
resultName,
typeObject,
type;
for(type in types) {
result = types[type];
resultName = self.normalizeName(result.name);
if(typeName === resultName) {
typeObject = result;
break;
}
}
defer.resolve(typeObject);
}, function (err) {
// the called failed
defer.reject(err);
})
return defer.promise;
};
return ProductType;
}]);
Taken from the angular docs https://docs.angularjs.org/api/ngResource/service/$resource
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data.
I'm relatively new to Backbone and though I know the general idea of how to use it, my learning has been rapid and I'm probably missing some key elements.
So I have a collection that contains an attribute called "type" which can be article, book, video, class. I have the view rendering and everything but I need to be able to filter the collection when links are clicked.
My question is - how can I get it to filter down the collection and still be able to refilter the original collection when I click on another type?
Here's the gist of my code, I simplified it for easy reading:
var TagsView = Backbone.View.extend({
initialize: function(query) {
this.collection = new TagsCollection([], {query: self.apiQuery} );
this.collection.on('sync', function() {
self.render();
});
this.collection.on('reset', this.render, this);
},
render: function() {
//renders the template just fine
},
filter: function() {
//filtered does work correctly the first time I click on it but not the second.
var filtered = this.collection.where({'type':filter});
this.collection.reset(filtered);
}
});
update: I managed to get this working. I ended up triggering a filter event.
var TagsCollection = Backbone.Collection.extend({
initialize: function(model, options) {
this.query = options.query;
this.fetch();
},
url: function() {
return '/api/assets?tag=' + this.query;
},
filterBy: function(filter) {
filtered = this.filter(function(asset) {
return asset.get('type') == filter;
});
this.trigger('filter');
return new TagsCollection(filtered, {query: this.query});
},
model: AssetModel
});
And then in my view, I added some stuff to render my new collection.
var TagsView = Backbone.View.extend({
initialize: function(query) {
this.collection = new TagsCollection([], {query: self.apiQuery} );
this.collection.on('sync', function() {
self.render();
});
this.collection.on('filter sync', this.filterTemplate, this);
this.collection.on('reset', this.render, this);
},
render: function() {
//renders the template just fine
},
filterCollection: function(target) {
var filter = $(target).text().toLowerCase().slice(0,-1);
if (filter != 'al') {
var filtered = this.collection.filterBy(filter);
} else {
this.render();
}
},
filterTemplate: function() {
filterResults = new TagsCollection(filtered, {query: self.apiQuery});
console.log(filterResults);
$('.asset').remove();
filterResults.each(function(asset,index) {
dust.render('dust/academy-card', asset.toJSON(), function(error,output) {
self.$el.append(output);
});
});
},
});
The reason it's not working a second time is because you're deleting the models that don't match your filter when you call reset. That's normal behaviour for the reset function.
Instead of rendering with the view's main collection, try using a second collection just for rendering which represents the filtered data of the original base collection. So your view MIGHT look something like:
var TagsView = Backbone.View.extend({
filter: null,
events: {
'click .filter-button': 'filter'
},
initialize: function (query) {
this.baseCollection = new TagsCollection([], {query: self.apiQuery} );
this.baseCollection.on('reset sync', this.filterCollection, this);
this.collection = new Backbone.Collection;
this.collection.on('reset', this.render, this);
},
render: function () {
var self = this,
data = this.collection.toJSON();
// This renders all models in the one template
dust.render('some-template', data, function (error, output) {
self.$el.append(output);
});
},
filter: function (e) {
// Grab filter from data attribute or however else you prefer
this.filter = $(e.currentTarget).attr('data-filter');
this.filterCollection();
},
filterCollection: function () {
var filtered;
if (this.filter) {
filtered = this.baseCollection.where({'type': this.filter});
} else {
filtered = this.baseCollection.models;
}
this.collection.reset(filtered);
}
});
To remove any filters, set a button with class filter-button to have an empty data-filter attribute. collection will then be reset with all of baseCollection's models
Here's a better answer to this. Instead of making it so complicated, you can just use the where method. Here's my replacement solution for the question above.
filterby: function(type) {
return type === 'all' ? this : new BaseCollection(this.where({type: type});
});
You can try using comparator function of your Collection.
http://backbonejs.org/#Collection-comparator
Basically its is like sorting your collection.