I've got a collection of Delivery models called DeliveryList. When I add or edit a Delivery, all attributes of the previously added or edited Delivery are overwritten by the attributes of the new one.
Curiously, if I reload the page after saving a model with this line of code:
// Hacky way to get around the models overwriting each other
location.reload();
The model will not be overwritten by newly created or edited models.
Any thoughts on why this is happening?
Here's the rest of my code:
var DeliveryView = Marionette.ItemView.extend({
initialize: function () {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
_.bindAll(this, "editDeliveryOption", "saveAllFields");
},
onRender: function() {
if (this.model.isNew()) {
this.editDeliveryOption();
this.$el.addClass("new");
}
},
template: "#delivery-item-template",
events: {
"click #removeThis": "removeDeliveryOption",
"click #editThis": "editDeliveryOption"
},
saveAllFields: function() {
var value = $("#optionName input").val();
this.model.save({ optionName: value });
var value = $("#shipToState option:selected").val();
this.model.save({ shipToState: value });
var value = $("#zipCodes input").val();
this.model.save({ zipCodes: value });
var value = $("#perOrderFee input").val();
this.model.save({ perOrderFee: value });
var value = $("#perItemFee input").val();
this.model.save({ perItemFee: value });
// After done editing, remove the view from the dom
this.editDeliveryForm.remove();
// Show the new option
this.$el.removeClass("new");
// Hacky way to get around the models overwriting each other
location.reload();
},
editDeliveryOption: function () {
this.editDeliveryForm = new Backbone.Form({
template: _.template($("#editDeliveryTemplate").html()),
model: this.model
}).render();
layout.editDelivery.show(this.editDeliveryForm);
$("#triggerEditDelivery").fancybox({
'afterClose': this.saveAllFields,
}).click();
// This button in Fancybox isn't working
$("#saveDelivery").click(function() {
this.saveAllFields;
});
},
removeDeliveryOption: function () {
this.model.destroy();
}
});
var DeliveriesView = Marionette.CompositeView.extend({
initialize: function () {
this.collection.fetch();
this.listenTo(this.collection, 'change', this.changThis);
},
changeThis: function () {
alert("it changed");
},
template: "#deliveries-view-template",
itemView: DeliveryView,
events: {
"click #addShipping": "addDeliveryOption",
},
addDeliveryOption: function() {
this.collection.create();
},
// Specify a jQuery selector to put the itemView instances in to
itemViewContainer: "#deliveries",
});
Thanks EmptyArsenal and mu is too short for pointing me in the right direction.
What ended up being the problem was the fancybox call:
$("#triggerEditDelivery").fancybox({
'afterClose': this.saveAllFields,
}).click();
Every time I added a new field, it kept binding a saveAllFields method call to #triggerEditDelivery. Therefore, every time I clicked #triggerEditDelivery for a new Delivery, it would save all them to the currently open one.
Here's my fix:
$("#triggerEditDelivery").fancybox({
helpers: {
overlay: { closeClick: false }
}
}).click();
$("#saveDelivery").click(this.saveAllFields);
$("#cancelDelivery").click(this.cancelDeliveryOption);
Related
I am running the following view:
app.OrganisationTab = Backbone.View.extend({
el : "#organisations",
template : _.template( $("#tpl-groups-list").html() ),
events : {
"click .js-edit-group" : "editGroup"
},
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
var that = this;
this.collection.fetch({
success: function() {
that.render();
}
})
},
change: function() {
//this.$el.empty();
console.log("collection has changed");
},
render:function() {
this.$el.empty();
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne, this);
},
addOne: function(model) {
var view = new app.GroupEntry({
model: model
});
this.$el.append(view.render().el);
},
editGroup: function(e) {
e.preventDefault();
var elm = $(e.currentTarget),
that = this;
$('#myModal').on('hidden.bs.modal', function () {
$('.modal-body').remove();
});
var organisation = this.collection.findWhere({ id : String(elm.data('groupid')) });
var members = organisation.get('users');
organisation.set('members', new app.UserCollection(members));
var projects = organisation.get('projects');
organisation.set('projects', new ProjectCollection(projects));
var orgForm = new app.createOrganisationForm({
model : organisation,
});
$('#myModal').modal({
backdrop: 'static',
keyboard: false
});
}
});
This view triggers a new view, and in that I can change a model save it (sends a PUT) and I can get in my console, collection has changed. If I console.log this collection I can see that the collection has changed. If I try and re-render the page all I see are the models as they were without the edits.
Why would this be happening, when clearly the collection is getting changes as it fires the events and I can see it when I log the collection?
After reading your comment:
No sorry on collection change I try to run render() which should empty
the container, and add all the models...but it seems to render the old
collection again.
You're getting this problem because you are overriding the success handler for the fetch call. That success callback is triggered before the models are placed in the collection. You need to listen to the sync event if you want render after the collection has been synchronized with the server (models are updated after fetch).
Update initialize to:
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
I am getting this error . I am able to preform read, and remove functions using BackboneJs , but i am having error when i execute the add method any help will be appreciated.
JSfiddel path is http://jsfiddle.net/2wjdcgky/
BackboneJS Uncaught Error: A "url" property or function must be specified
$(function() {
Model
var modelContact = Backbone.Model.extend({
defaults: function() {
return {
Id: 0,
Name: "",
Address: ""
};
},
idAttribute: "Id"
});
ModelCollection
var contactCollection = Backbone.Collection.extend({
model: modelContact,
url: function() {
return 'api/Contact';
},
add: function(model) {
this.sync("create", model); // Error On create
},
remove: function(model) {
this.sync("delete", model); //Runs Fine
}
});
var contacts = new contactCollection;
View
var contactView = Backbone.View.extend({
tagName: "tr",
events: {
"click a.destroy": "clear"
},
template: _.template($("#newContacttemplate").html()),
initialize: function() {
this.model.on("change", this.render, this);
this.model.on('destroy', this.remove, this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function(e) {
contacts.remove(this.model); // runs fine
}
});
Main View
var main = Backbone.View.extend({
el: $("#contactApp"),
events: {
"click #btnsave": "CreateNewContact"
},
initialize: function() {
this.Nameinput = this.$("#contactname");
this.Addressinput = this.$("#contactaddress");
contacts.on("add", this.AddContact, this);
contacts.on("reset", this.AddContacts, this);
contacts.fetch();
},
AddContact: function (contact) {
console.log("AddContact");
var view = new contactView({ model: contact });
this.$("#tblcontact tbody").append(view.render().el);
},
AddContacts: function () {
console.log("AddContacts");
contacts.each(this.AddContact);
},
CreateNewContact: function (e) {
console.log(e);
//Generate an error "BackboneJS Uncaught Error: A "url" property or function must be specified"
contacts.add({ Name: this.Nameinput.val(), Address: this.Addressinput.val() });
}
});
var m = new main;
});
Your JSFiddle was missing Backbone references and all.
Working update: http://jsfiddle.net/apt7hchL/2/
Much simpler code (no need to define those add and remove methods on the collection!). Also more common Javascript coding style conventions.
Please note I had to manually generate an "Id" attribute to allow creating more than one contact. As you are making Id = 0 by default, second model with same is not added, as Backbone sees a model with id=0 is already in the collection.
When you want to save, call the model.save() method. Don't call sync manually, you'll normally don't need to!
For the model to be saved to the database before being added to the collection, use:
createNewContact: function (e) {
e.preventDefault();
var self = this;
var newContact = new ContactModel({
Name: this.$("#name").val(),
Address: this.$("#address").val()
});
newContact.save({ success: function(model){
self.collection.add(model);
});
//clear form
this.$("#name").val("");
this.$("#address").val("");
}
Sync method tries to sync to a server setup to handle it, with CRUD abilities. If thats not what you're looking for, and you just want to display this information on the client side, instead of using sync, you should use Collection.add(model) and Collection.remove(model)
I have a few tabs on my page, whose contents (consisting of many SetView contained by SetListView) are loaded using Backbone.js whenever its tabs is being clicked on.
Problem:: When the user switches from a tab to a previously loaded/viewed tabbed, the contents load again and append to the previously loaded content in SetListView. I can get it to clear the previously loaded contents before loading it again, but it seems to be less than optimal to keep loading the same content.
Is it possible to make Backbone.js store existing content for a tab and not load it multiple times when switching back to the same tab?
Views
// Views
var SetListView = Backbone.View.extend({
el: '#set_list',
initialize: function() {
this.collection.bind('reset', this.render, this);
},
render: function() {
this.collection.each(function(set, index) {
$(this.el).append( new SetView({ model: set }).render().el );
}, this);
return this;
}
});
var SetView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
template: _.template( $('#tpl_SetView').html() ),
initialize: function() {
this.model.on('destroy', this.close, this);
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
return this;
},
close: function() {
this.unbind();
this.remove();
}
});
Router
// Router
var AppRouter = Backbone.Router.extend({
routes: {
'': 'sets',
'sets': 'sets'
},
viewing_user_id: $('#viewing_user_id').val(),
sets: function() {
this.showTab('sets');
this.setList = new SetCollection();
this.setListView = new SetListView({ collection: this.setList });
var self = this;
this.setList.fetch({
data: {user_id: self.viewing_user_id},
processData: true
});
},
showTab: function(tab) {
// Show/hide tab contents
$('.tab-content').children().not('#tab_pane_' + tab).hide();
$('.tab-content').children('#tab_pane_' + tab).fadeIn('fast');
// Activate/deactivate tabs
$('#tab_' + tab).addClass('active');
$('#tab_' + tab).siblings().removeClass('active');
}
});
Backbone has not any in-house system to difference between when you want to re-fetch the content or re-using the already fetched one. You have to decide when do each of this actions.
A modification of your example code to achieve this can be:
var AppRouter = Backbone.Router.extend({
// ... more router code
sets: function() {
if( !this.setList ) this.initializeSets();
this.showTab('sets');
},
initializeSets: function(){
this.setList = new SetCollection();
this.setListView = new SetListView({ collection: this.setList });
var self = this;
this.setList.fetch({
data: {user_id: self.viewing_user_id},
processData: true
});
},
});
So you only call initializeSets() if they are not already initialized. Of course will be more elegant and clean ways to ask if the sets have been initialized but this is up to you.
I've got a little problem here:
In my Backbone.js app I save changes in a content editable on blur. This means, that when pressing the tab key the whole view is re-rendered and I loose the focus on the next element. How can I restore this?
You can maintain a property, either in the view (as a plain attribute, as in the example below) or model, to store the currently focused element. Whenever focus changes, update the property.
After re-rendering stuff, set the focus to the element manually.
Here is a minimal code:
var myView = Backbone.View.extend({
el: $('#formElement'),
initialize: function() {
_.bindAll(this);
}
events: {
'focus input': "updateFocus"
},
updateFocus: function(event) {
this.focusedElem = $(event.target);
},
render: function() {
// After rendering is complete
this.focusedElem.focus();
}
});
I use a dedicated ViewModel and View for every input. It has a special readValue/writeValue methods which update element instead of recreating it. It looks this way:
var TextInput = Backbone.Model.extend({ // abstract
defaults: {
value: '', // text
visible: true, // determines if input element is visible
readonly: false, // determines if input element is read only
enabled: true, // determines if input element is enabled
delay: 750 // view/model sync latency
}
});
var TextInputView = Backbone.View.extend({
template: _.template($('#text-input').html()),
initialize: function (options) {
this.model.bind('change:visible', this.render, this);
this.model.bind('change:readonly', this.render, this);
this.model.bind('change:enabled', this.render, this);
this.model.bind('change:value', this.readValue, this);
},
events: {
'change input': 'writeValue',
'keyup input': 'writeValue'
},
render: function () {
$(this.el).html(this.template(this.model))
.find('input')
.prop({
readonly: this.model.get('readonly'),
disabled: !this.model.get('enabled')
})
.toggleClass('hidden', !this.model.get('visible'));
this.readValue();
return this;
},
changeTimer: null,
writeValue: function () {
if (this.changeTimer)
clearTimeout(this.changeTimer);
var that = this;
this.changeTimer = setTimeout(function () {
that.model.set({ value: that.$('input').val() });
}, this.model.get('delay'));
},
readValue: function () {
if (this.$('input').val() != this.model.get('value'))
this.$('input').val(this.model.get('value'));
}
});
I found that I wanted it to go to the "next" element after rendering. Also, you can't remember an element in JQuery that gets removed from the DOM. So I record the name of the input instead of the input itself. Combining the previous answers you can do something similar to below. Remember I have some assumptions in there, like names on the inputs and that I search within the fieldset.
getNextInputForName = function(desiredName) {
var desiredElement = false;
var foundElement;
$("fieldset input").each(function(index) {
if (desiredElement) {
foundElement = $(this);
return false;
}
if ($(this).attr("name") === desiredName) {
desiredElement = true;
}
});
return foundElement;
}
var myView = Backbone.View.extend({
el: $('#formElement'),
initialize: function() {
_.bindAll(this);
}
events: {
'focus input': "updateFocus"
},
updateFocus: function(event) {
this.focusedElem = $(event.target).attr("name");
},
render: function() {
// After rendering is complete
if( this.focusedElem ) {
getNextInputForName(this.focusedElem).focus();
}
}
});
I'm new to backbone.js and MVC so apologise if this is a silly question...
I have been experimenting with some of the backbone.js tutorials out there and am trying to work out how to load an initial set of data onto the page.
If anyone could point me in the right direction or show me the what I'm missing below, it would be greatly appreciated!
Thanks!
The code is below or at: http://jsfiddle.net/kiwi/kgVgY/1/
The HTML:
Add list item
The JS:
(function($) {
Backbone.sync = function(method, model, success, error) {
success();
}
var Item = Backbone.Model.extend({
defaults: {
createdOn: 'Date',
createdBy: 'Name'
}
});
var List = Backbone.Collection.extend({
model: Item
});
// ------------
// ItemView
// ------------
var ItemView = Backbone.View.extend({
tagName: 'li',
// name of tag to be created
events: {
'click span.delete': 'remove'
},
// `initialize()` now binds model change/removal to the corresponding handlers below.
initialize: function() {
_.bindAll(this, 'render', 'unrender', 'remove'); // every function that uses 'this' as the current object should be in here
this.model.bind('change', this.render);
this.model.bind('remove', this.unrender);
},
// `render()` now includes two extra `span`s corresponding to the actions swap and delete.
render: function() {
$(this.el).html('<span">' + this.model.get('planStartDate') + ' ' + this.model.get('planActivity') + '</span> <span class="delete">[delete]</span>');
return this; // for chainable calls, like .render().el
},
// `unrender()`: Makes Model remove itself from the DOM.
unrender: function() {
$(this.el).remove();
},
// `remove()`: We use the method `destroy()` to remove a model from its collection.
remove: function() {
this.model.destroy();
}
});
// ------------
// ListView
// ------------
var ListView = Backbone.View.extend({
el: $('body'),
// el attaches to existing element
events: {
'click button#add': 'addItem'
},
initialize: function() {
_.bindAll(this, 'render', 'addItem', 'appendItem'); // every function that uses 'this' as the current object should be in here
this.collection = new List();
this.collection.bind('add', this.appendItem); // collection event binder
this.render();
},
render: function() {
_(this.collection.models).each(function(item) { // in case collection is not empty
appendItem(item);
}, this);
},
addItem: function() {
var item = new Item();
var planStartDate = $('#planStartDate').val();
var planActivity = $('#planActivity').val();
item.set({
planStartDate: planStartDate,
planActivity: planActivity
});
this.collection.add(item);
},
appendItem: function(item) {
var itemView = new ItemView({
model: item
});
$('ul', this.el).append(itemView.render().el);
}
});
var listView = new ListView();
})(jQuery);
Thanks.
Here's the modified example: http://jsfiddle.net/kgVgY/2/
You create the collection first with the data you want
var list = new List([{
createdOn: 'Jan',
createdBy: 'John',
planStartDate: "dfd",
planActivity: "dfdfd"
}]);
and then pass the collection to the view you want
var listView = new ListView({collection: list});
That's about all you had wrong in this code. Few minor unrelated notes:
You were using _(this.collection.models).each. Backbone collections use underscore to expose all those functions on themselves, so that is equivalent to this.collection.each
You don't really need the "unrender" method on the ItemView but since you aren't using that I'm guessing you're using it for debugging.