Backbone Collection Can't Remove Items - javascript

I've got a Backbone Model called Delivery. I then create a collection of Deliveries called DeliveryList backed by LocalStorage. In my Marionette.ItemView for displaying items in the collection, I have a method to remove items:
removeDeliveryOption: function() {
Deliveries.remove(this.model.get("id"));
}
For some reason, this removes the item from the Marionette.CompositeView when I click the remove button, but when I reload the page the same number of items always reappear.
It's worth noting that when I delete the item, it always reappears with the default optionName "Free Delivery". I'm using both defaults and a schema in the model because I'm using the Backbone-forms plugin (https://github.com/powmedia/backbone-forms).
Any help is greatly appreciated!
var Delivery = Backbone.Model.extend({
defaults: function () {
return {
order: Deliveries.nextOrder(),
optionName: "Free Delivery",
shipToState: "Hawaii",
zipCodes: "96813",
perOrderFee: "0.00",
perItemFee: "0.00"
};
},
schema: {
optionName: { type: 'Text', validators: ['required'] },
shipToState: { type: 'Select', options: getStateNames(), validators: ['required'] },
zipCodes: { type: 'Text', validators: ['required'] },
perOrderFee: { type: 'Text', validators: ['required'] },
perItemFee: { type: 'Text', validators: ['required'] },
}
});
var DeliveryList = Backbone.Collection.extend({
model: Delivery,
localStorage: new Backbone.LocalStorage("deliverylist-backbone"),
nextOrder: function () {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: 'order'
});
var Deliveries = new DeliveryList;
var deliveryView = Marionette.ItemView.extend({
//tagName: "li",
template: "#delivery-item-template",
events: {
"click #removeThis": "removeDeliveryOption",
},
removeDeliveryOption: function() {
Deliveries.remove(this.model.get("id"));
}
});
var DeliveriesView = Marionette.CompositeView.extend({
initialize: function() {
Deliveries.fetch();
},
template: '#deliveries-view-template',
itemView: deliveryView,
events: {
"click #addShipping": "addDeliveryOption",
},
addDeliveryOption: function() {
var editDeliveryForm = new Backbone.Form({
template: _.template($("#editDeliveryTemplate").html()),
model: Deliveries.create()
}).render();
this.$el.append(editDeliveryForm.el);
$("#triggerEditDelivery").fancybox({
'afterClose': function () {
commitForm(editDeliveryForm);
//Wait do display the inlineModel until here
// Once we've bound the form to the model, put the saving logic with the collection
//Deliveries.last().save();
}
}).trigger('click');
},
// Specify a jQuery selector to put the itemView instances in to
itemViewContainer: "#deliveries",
});
EDIT
Thanks to #ejosafat! Had to destroy the model instead of just removing from collection.
removeDeliveryOption: function() {
this.model.destroy();
}

The remove method only affects the collection loaded in the browser, not in the permanent storage (local or server). That's why it dissappears from the view but when you reload the page it appears again.
If you want to get rid of that model in the storage too, use its destroy method.
(btw, it's a common convention in Javascript to use initial capital letter only for constructor functions, as clue that it should be used with the new operator, or be extended to create a derived constructor/class, so it's a bad idea to use Deliveries as a collection var name)

Related

Saving Only the changed record on a BackGrid grid?

I am in the process of learning Backbone.js and using BackGrid to render data and provide the end user a way to edit records on an Microsoft MVC website. For the purposes of this test grid I am using a Vendor model. The BackGrid makes the data editable by default (which is good for my purpose). I have added the following JavaScript to my view.
var Vendor = Backbone.Model.extend({
initialize: function () {
Backbone.Model.prototype.initialize.apply(this, arguments);
this.on("change", function (model, options) {
if (options && options.save === false) return;
model.url = "/Vendor/BackGridSave";
model.save();
});
}
});
var PageableVendors = Backbone.PageableCollection.extend(
{
model: Vendor,
url: "/Vendor/IndexJson",
state: {
pageSize: 3
},
mode: "client" // page entirely on the client side.
});
var pageableVendors = new PageableVendors();
//{ data: "ID" },
//{ data: "ClientID" },
//{ data: "CarrierID" },
//{ data: "Number" },
//{ data: "Name" },
//{ data: "IsActive" }
var columns = [
{
name: "ID", // The key of the model attribute
label: "ID", // The name to display in the header
editable: false, // By default every cell in a column is editable, but *ID* shouldn't be
// Defines a cell type, and ID is displayed as an integer without the ',' separating 1000s.
cell: Backgrid.IntegerCell.extend({
orderSeparator: ''
})
}, {
name: "ClientID",
label: "ClientID",
cell: "integer" // An integer cell is a number cell that displays humanized integers
}, {
name: "CarrierID",
label: "CarrierID",
cell: "number" // A cell type for floating point value, defaults to have a precision 2 decimal numbers
}, {
name: "Number",
label: "Number",
cell: "string"
}, {
name: "Name",
label: "Name",
cell: "string"
},
{
name: "IsActive",
label: "IsActive",
cell: "boolean"
}
];
// initialize a new grid instance.
var pageableGrid = new Backgrid.Grid({
columns: [
{
name:"",
cell: "select-row",
headercell: "select-all"
}].concat(columns),
collection: pageableVendors
});
// render the grid.
var $p = $("#vendor-grid").append(pageableGrid.render().el);
// Initialize the paginator
var paginator = new Backgrid.Extension.Paginator({
collection: pageableVendors
});
// Render the paginator
$p.after(paginator.render().el);
// Initialize a client-side filter to filter on the client
// mode pageable collection's cache.
var filter = new Backgrid.Extension.ClientSideFilter({
collection: pageableVendors,
fields: ['Name']
});
// REnder the filter.
$p.before(filter.render().el);
//Add some space to the filter and move it to teh right.
$(filter.el).css({ float: "right", margin: "20px" });
// Fetch some data
pageableVendors.fetch({ reset: true });
#{
ViewBag.Title = "BackGridIndex";
}
<h2>BackGridIndex</h2>
<div id="vendor-grid"></div>
#section styles {
#Styles.Render("~/Scripts/backgrid.css")
#Styles.Render("~/Scripts/backgrid-select-all.min.css")
#Styles.Render("~/Scripts/backgrid-filter.min.css")
#Styles.Render("~/Scripts/backgrid-paginator.min.css")
}
#section scripts {
#Scripts.Render("~/Scripts/underscore.min.js")
#Scripts.Render("~/Scripts/backbone.min.js")
#Scripts.Render("~/Scripts/backgrid.js")
#Scripts.Render("~/Scripts/backgrid-select-all.min.js")
#Scripts.Render("~/Scripts/backbone.paginator.min.js")
#Scripts.Render("~/Scripts/backgrid-paginator.min.js")
#Scripts.Render("~/Scripts/backgrid-filter.min.js")
#Scripts.Render("~/Scripts/Robbys/BackGridIndex.js")
}
When the user edits a row, it successfully fires the hits the model.Save() method and passes the model to the save Action, in this case BackGridSave and it successfully saves the record that changed, but seems to save all of the vendors in model when only one of the vendors changed. Is there a way from the JavaScript/Backbone.js/BackGrid to only pass one Vendor - the vendor that changed?
Update: I realized that it is not sending every vendor, but it is sending the same vendor multiple times as though the change event was firing multiple times.
I guess I answered my own question. Well, at least I am getting the desired result. I just added a call to off after the first on. Seems like this would not be necessary though.
var Vendor = Backbone.Model.extend({
initialize: function () {
Backbone.Model.prototype.initialize.apply(this, arguments);
this.on("change", function (model, options) {
if (options && options.save === false) return;
model.url = "/Robbys/BackGridSave";
model.save();
model.off("change", null, this); // prevent the change event from being triggered many times.
});
}
});

How to add function on table fields - Backbone.js

I have this code in a Backbone application that I need to debug. (rough idea)
window.TableView = Backbone.View.extend({
initialize: function() {...
..
..
...
});
},
selectRow: function() {
...
...
..
},
render: function() { // this renders my models fields in a table
var editableColumns = [
//{ name: "display", type: "combobox", combobox: comboboxOptions, validate: validateText },
{ name: "display" },
{ name: "submitDate", type: "datepicker", datepicker: datepickerOptions },
{ name: "displayDate", type: "datepicker", datepicker: datepickerOptions },
{ name: "name"},
...
...
Now my problem is, how can I add a function to this field: { name: "display" }
like onclick, or after focus function, etc.? For example can I have,
{ name: "display", onclick: setMyText(); } or something like this? Also is this part of backbone.js or one of its components? Where can I read more about this?
In Backbone you have events hash for a View where you can specify the events for respective View. Events are specified in following format:
{"event selector": "callback"}
So in your case for all the editableColumns you also need a selector for each one or may you can specify by using the name property. Try specifying the events hash like this :
events: {
'click .columnSelector[name="display"]' : "setMyText"
}
where .columnSelector is the class applied to element that is to be edited.
For more details on events check this.

Backbone.Marionette view with subviews

What is the apropriate aproach to setup a view in a Backbone.Marionete environment to have a list of subviews, without manually rendering them, and consume as least as possible memmory.
The view with child views is rendered based on a template, and is a part of a tab control tabs. The tamplete for the tab view has divs, which are used as a placholders for child controls ( two collection views and two helper controls )
Several aproaches I've made already:
1) Create view instances in render method and, attach them to a propper el hardcoding the selectors in render method.
2) Extend a marionete layout and declare a regions for each view.
var GoalsView = Marionette.Layout.extend({
template: '#goals-view-template',
regions: {
content: '#team-goals-content',
homeFilter: '#team-goals-home-filter',
awayFilter: '#team-goals-away-filter'
},
className: 'team-goals',
initialize: function () {
this.homeFilterView = new SwitchControlView({
left: { name: 'HOME', key: 'home' },
right: { name: 'ALL', key: 'all' },
});
this.awayFilterView = new SwitchControlView({
left: { name: 'AWAY', key: 'away' },
right: { name: 'ALL', key: 'all' },
});
this.сontentView = new GoalsCollecitonView({
collection: statsHandler.getGoalsPerTeam()
});
},
onShow: function () {
this.content.show(this.сontentView);
this.homeFilter.show(this.homeFilterView);
this.awayFilter.show(this.awayFilterView);
}
});
This is the cool way, but I am worried about the overhead for maintaing regions collection which will always display single view.
3) I extended marionette item view with the following logic:
var ControlsView = Marionette.ItemView.extend({
views: {},
onRender: function() {
this.bindUIElements();
for (var key in this.ui) {
var view = this.views[key];
if (view) {
var rendered = view.render().$el;
//if (rendered.is('div') && !rendered.attr('class') && !rendered.attr('id')) {
// rendered = rendered.children();
//}
this.ui[key].html(rendered);
}
}
}
});
Which allowed me to write following code
var AssistsView = ControlsView.extend({
template: '#assists-view-template',
className: 'team-assists',
ui: {
content: '#team-assists-content',
homeFilter: '#team-assists-home-filter',
awayFilter: '#team-assists-away-filter'
},
initialize: function () {
this.views = {};
this.views.homeFilter = new SwitchControlView({
left: { name: 'HOME', key: 'home' },
right: { name: 'ALL', key: 'all' },
});
this.views.awayFilter = new SwitchControlView({
left: { name: 'AWAY', key: 'away' },
right: { name: 'ALL', key: 'all' },
});
this.views.content = new AssistsCollecitonView({
collection: statsHandler.getAssistsPerTeam()
});
}
});
But it will leak memmory for sure, and I not feel like I will be able to write proper code to handle memmory leaks.
So in general, what I want, is to have a nice declarative way to create a view with other views as controls on it, with protection agains memmory leaks and least memmory consumption possible...
P.S. sorry for the wall of text
Why don't you simply use a layout and display your views within the layout's regions? You can see an example here: https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js#L43-L46

Backbone Collection is populated with models but cannot pull JSON out of it... simple answer I'm sure

Thank you for all the help this community has given me, I am extremely grateful. If anyone knows what im doing incorrectly I would love to figure this out :)
The Problem:
I just started using backbone. lol But really, I am trying building and app that uses full calendar and backbone to populate the calendar. Everything is going smooth so far and I am able to add new sessions with the correct start and end dates to the calendar.
But on the initial load none of these session models populate on the calender. Here is a screenshot of what I am getting in console.log:
As you can see when I call jus the collection you can see it is full of models. But when I try to convert to JSON it comes up empty. I have found numerous answers on Stack Overflow about this and none of them seems to make this work. I am officially roadblocked. lol
Here is some code im working with to display collection
var Event = Backbone.Model.extend({
methodToURL: {
'create': addDayURL,
'update': addDayURL,
//'delete': '/user/remove'
},
sync: function(method, model, options) {
options = options || {};
options.url = model.methodToURL[method.toLowerCase()];
Backbone.sync(method, model, options);
}
});
var Events = Backbone.Collection.extend({
model: Event,
url: allDaysURL
});
The view is huge so I wil only include the relavent parts:
var EventsView = Backbone.View.extend({
events: {
'click #add_track' : "addTrack"
},
initialize: function(){
_.bindAll(this);
this.collection.bind('reset', this.addAll);
this.collection.bind('add', this.addOne);
this.collection.bind('change', this.change);
this.collection.bind('destroy', this.destroy);
this.eventView = new EventView();
console.log('this.collection: ', this.collection);
console.log('this.collection.toJSON(): ', this.collection.toJSON());
console.log('JSON.stringify(this.collection.toJSON()): ', JSON.stringify(this.collection.toJSON()));
//console.log(this.collection.toJSON())
// your model2 option: this.options.collection2.toJSON();
//console.log(this.options.collection2.toJSON());
},
render: function() {
this.$el.fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,basicWeek,basicDay',
},
//defaultView: 'resourceDay',
resources: //this.options.collection2.toJSON()
[
{
/*
* trackID
* name
* backgroundColor
* foregroundColor*/
id: 1,
name: 'Track 1',
color: 'red',
textColor: 'black'
},
{
id: 2,
name: 'Track 2',
color: 'blue'
},
{
id: 3,
name: 'Track 3',
color: 'pink'
},
{
id: 4,
name: 'Track 4',
color: 'green'
},
{
id: 5,
name: 'Track 5',
color: 'yellow',
textColor: 'black'
}
],
droppable: true,
selectable: true,
selectHelper: true,
editable: true,
ignoreTimezone: false,
select: this.select,
eventClick: this.eventClick,
eventDrop: this.eventDropOrResize,
eventResize: this.eventDropOrResize,
drop: function(date, allDay, ev, ui, res) { // this function is called when something is dropped
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// assign it the date that was reported
copiedEventObject.start = date;
copiedEventObject.allDay = allDay;
// dropped event of resource a to a cell belonging to resource b?
copiedEventObject.resourceId = res.id;
// render the event on the calendar
// the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/)
$('#calendar').fullCalendar('renderEvent', copiedEventObject, true);
// is the "remove after drop" checkbox checked?
if ($('#drop-remove')) {
// if so, remove the element from the "Draggable Events" list
$(this).remove();
}
this.addOne;
}
});
this.$el.prepend('<button id="add_track" class="btn large-btn green-btn pull-right">Add Track</button>');
},
addAll: function() {
this.$el.fullCalendar('addEventSource', this.collection.toJSON());
},
addOne: function(event) {
this.$el.fullCalendar('renderEvent', event.toJSON());
}
EDIT:
To initialize collection and populate this code resides at the bottom fo the file:
var events = new Events();
var tracks = new Tracks();
new EventsView({el: $("#calendar"), collection: events, collection2: tracks}).render();
new AddSessionView({ collection: events}).render();
events.fetch();
I'm also new to Backbone but maybe this helps. Have you tried to convert the models property of your Collection object to JSON, like:
JSON.stringify(collection.models)
This approach worked for me.
JSFiddle: http://jsfiddle.net/YcFmB/
try this :
var events = new Events();
var tracks = new Tracks();
events.fetch();
new EventsView({el: $("#calendar"), collection: events, collection2: tracks}).render();
new AddSessionView({ collection: events}).render();
I read that fetch() was an asynchronous operation, therefore when you fetch a collection, the ajax call will be sent and then your code will continue being run just like nothing happened.
I ended up creating my creating my first view inside the fetch() success handler:
events.fetch({success: function (models) {
tracks.fetch({success: function (models) {
new EventsView({el: $("#calendar"), collection: events, collection2: tracks}).render();
}});
}});
Could be an async issue in that events.fetch(); is returning after your views have been called. I use deferreds to deal with this.
var events = new Events();
var tracks = new Tracks();
$.when( events.fetch() )
.done( function(data) {
console.dir(data)
new EventsView({el: $("#calendar"), collection: events, collection2: tracks}).render();
new AddSessionView({ collection: events}).render();
});
Although, your bind on reset, should take care of that...
EDIT:
I would be curious to know what your console readout above was?

backbone.js collection usage

I'm running into a problem maintaining my collection. First, I load attendees into a collection via fetch. This loads existing attendees from the database in to the collection. I also have a button which allows a user to add new attendees. When an attendee is manually entered it seems to wipe out the models loaded into the collection via fetch and starts fresh. All manually added attendees now populate the collection; however, i would like both the fetch loaded and manually added attendees to populate this list.
var InviteeView = Backbone.View.extend({
tagName: "tr",
initialize: function() {
this.collection = new InviteeJSONList();
_.bindAll(this, 'render','appendItem','remove','saveInvitee');
},
events: {
"click .removeInvitee":"remove",
"click .saveInvitee":"saveInvitee"
},
render: function() {
var source = $("#invitee-template").html();
var template = Handlebars.compile(source);
var context = inviteeListJSON.attributes['json'];
var html=template(context);
$(this.el).html(html);
return this;
},
appendItem: function() {
$("#attendees").append(this.render().el);
},
remove: function() {
$(this.el).remove();
},
saveInvitee: function() {
var value = $(this.el).find('select').val();
var model = this.collection.attributes['json']['invitees'];
model = model.filter(function(attributes) {return attributes.encrypted_id==value});
var attendee = new Attendee({
user_id: model[0]['id'],
meeting_id: '<?=$mid?>',
status: 'Uncomfirmed',
first_name: model[0]['first_name'],
last_name: model[0]['last_name'],
email: model[0]['email'],
user_uuid: model[0]['encrypted_id'],
unavailable_dates: model[0]['unavailable_dates']
});
attendeeView.addAttendeeItem(attendee.attributes)
this.remove();
}
});
var AttendeeList = Backbone.Collection.extend({
model: Attendee,
url: '<?=$baseURL?>api/index.php/attendees/<?=$mid?>&timestamp=<?=$timestamp?>&userid=<?=$userid?>&apikey=<?=$apikey?>',
parse: function(response) {
if(response!="No History") {
$.each(response['attendees'], function(key, value) {
attendeeView.addAttendeeItem(value);
});
$('.loading_attendees').hide();
}
else {
$('.loading_attendees').html("No attendees exists for this meeting.");
}
}
});
var AttendeeView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render','fetchAttendees', 'appendItem', 'addAttendeeItem');
this.counter=0;
this.collection = new AttendeeList();
this.collection.bind('add', this.appendItem);
this.fetchAttendees();
},
events: {
"click #addInvitee":"appendInvitees",
},
appendInvitees: function() {
var inviteeView = new InviteeView();
inviteeView.appendItem();
},
render: function() {
},
fetchAttendees: function() {
this.collection.fetch({
success: function(model, response) {
},
error: function(model, response) {
$('#loading_attendees').html("An error has occurred.");
}
});
},
appendItem: function(item) {
var attendeeItemView = new AttendeeItemView({
model: item
});
$("#attendees").append(attendeeItemView.render().el);
attendeeItemView.updateAttendeeStatusSelect();
},
addAttendeeItem: function(data) {
this.counter++;
var attendee = new Attendee({
id: data['id'],
user_id: data['user_id'],
meeting_id: data['id'],
status: data['status'],
comments: data['comments'],
attended: data['datetime'],
first_name: data['first_name'],
last_name: data['last_name'],
email: data['email'],
counter: this.counter,
user_uuid: data['user_uuid'],
unavailable_dates: data['unavailable_dates']
});
this.collection.add(attendee);
},
});
After the collection (2 items loaded from REST API) is loaded via fetch():
console.log(this.collection.models) outputs:
[d]
[d,d]
Then when I manually add an attendee via a button the collection seems to reset:
console.log(this.collection.models) outputs:
[d]
Good that it's working, as there are many ways to go. I probably would have structured it a bit differently to leverage the Backbone methods that instantiate modes, but working code is the real goal, so these are just my thoughts:
Rather than actually instantiate the Models in the Collection parse() method, merely have parse return an array of data objects from which Backbone would instantiate the models, and trigger a
Rather than call fetch for the Collection inside AttendeeView, but outside the View class
Either have AttendeeView represent the view for a single attendee, or name it AttendeeListView and have it render the list
For instance:
AttendeeList = Backbone.Collection.extend({
...
parse: function(response) {
// create an array of objects from which the models can be parsed
var rawItems = [];
$.each(response['attendees'], function(key, value) {
rawItems.push({
id: data['id'],
user_id: data['user_id'],
meeting_id: data['id'],
status: data['status'],
comments: data['comments'],
attended: data['datetime'],
first_name: data['first_name'],
last_name: data['last_name'],
email: data['email'],
counter: this.counter,
user_uuid: data['user_uuid'],
unavailable_dates: data['unavailable_dates']
});
});
return rawItems;
},
...
}
and then either use the success/failure call backs:
AttendeeList.fetch( onListFetchSuccess , onListFetchFail );
or listen for the reset event that gets triggered:
AttendeeList.on('reset', createAttendeeListView );
(I didn't actually edit and run the code, this is just an outline)
I ended up resolving the issue by removing the url parameter and parse function out of the collection and into the view. Now everything works as expected.

Categories

Resources