I'm trying to add a new model to my parent view from a child view.
In my parent view, I render the child view like this:
renderEngineOptions: function () {
var view = new EngineOptionsView({ model: this.model });
this.$('.engineOptions').append(view.render().el);
},
In my child view(engineOptionsView), I define the parent as 'this.engine' in the initialize method:
initialize: function (args) {
this.engine = args.model;
}
Then I have an html button, that when clicked, fires this event:
addTurbo: function() {
this.$('.turboOption').show();
this.engine.turbo = new Turbo();
}
Turbo is a new model that I'm trying to add to the engine. But nothing happens. I get no error in the console, but when I write out the engine model in the console, no turbo was ever added.
How can I add the turbo model to the parent, from the child?
Thanks!
When you pass a model with the model option, the view automatically apply it to itself, so it's available through this.model directly.
var view = new EngineOptionsView({ model: this.model });
in the child:
initialize: function (args) {
this.engine = args.model; // not necessary
console.log(this.model === this.engine); // same thing
}
You are adding the turbo as a property of the model
this.engine.turbo = new Turbo();
So if you want to get the turbo from the parent:
this.model.turbo;
If you want to add the turbo as an attribute of the model from the child view:
this.model.set('turbo', new Turbo());
And to get it from the parent:
this.model.get('turbo);
Without more details, it's hard to guess where the problem is coming from.
Related
I have a collection of items. I would like to keep track of the current selection. When the user clicks on a different item in the collection, I want to indicate that the item is selected and display the details of the selected item. Think of this as a list with a detail view (like a typical email client).
Example of a master-detail layout (source):
I currently have something like this (written in CoffeeScript, templates use haml-coffee):
class Collections.Items extends Backbone.Collection
model: Models.Item
setCurrentSelection: (id)->
# what to do here? Is this even the right way to do it?
getCurrentSelection: ->
# what to do here? Is this even the right way to do it?
class Views.ItemsPage extends Backbone.View
list_template: JST['items/list']
details_template: JST['items/details']
events:
'click .item': 'updateSelection'
initialize: (options)->
#collection = options.collection
render: ->
$('#items_list').html(#list_template(collection: #collection.toJSON())) # not sure if this is how to render a collection
$('#item_details').html(#details_template(item: #collection.currentSelection().toJSON())) # how to implement currentSelection?
#
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
# mark the item as selected
# re-render using the new selection
# templates/items/list.hamlc
%ul
- for item in #collection
%li{data:{id: item.id}, class: ('selected' if item.selected?)} # TODO: How to check if selected?
= item.name
# templates/items/details.hamlc
%h2= #item.name
I'm not sure if I'm following you (my CoffeeScript is a bit rusty), but I think what you're trying to do is set a selected property on the appropriate model in your updateSelection method, and then re-render your view.
In other words:
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
model = this.collection.get(item_id) # get the model to select
model.selected = true # mark the item as selected
this.render() # re-render using the new selection
even saying "my CoffeeScript is a bit rusty" is too much for me. But i'll still attempt to explain as best as i can in js.
First the backbone way is to keep models as a representation of a REST resource document. (server side - persisted data).
Client side presentation logic should stick to views. to remember which list item is visible in in the details part is job of the that specific view. initiating change request for details view model is job of the list of items.
the ideal way is to have two separate views for list and details. (you can also go a bit more ahead and have a view for every item in the list view.
parent view
var PageView = Backbone.View.extend({
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
this.details = new ItemDetailView({
model : this.collection.at(1) //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
},
render: function() {
this.$el.html(this.template); // or this.$el.empty() if you have no template
this.renderList();
this.renderDetails();
},
renderList : function(){
this.$('#items_list').append(this.list.$el); //or any other jquery way to insert
this.list.render();
},
renderDetails : function(){
this.$('#item_details').append(this.details.$el); //or any other jquery way to insert
this.details.render();
}
});
list view
var ItemListView = Backbone.View.extend({
events : {
'click .item': 'updateSelection'
},
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
updateSelection : function(){
var selectedModel;
// a mechanism to get the selected model here - can be same as yours with getting id from data attribute
// or you can have a child view setup for each model in the collection. which will trigger an event on click.
// such event will be first captured by the collection view and thn retriggerd for page view to listen.
this.trigger('itemSelect', selectedModel);
}
});
details view
var ItemDetailView = Backbone.View.extend({
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
});
This won't persist the state through routes if you don't reuse your views. in that case you need to have a global state/event saving mechanism. somthing like following -
window.AppState = {};
_.extend(window.AppState, Backbone.Events);
//now your PageView initilize method becomes something like this -
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
var firstModel;
if(window.AppState.SelectedModelId) {
firstModel = this.collection.get(window.AppState.SelectedModelId);
} else {
firstModel = this.collection.at(1);
}
this.details = new ItemDetailView({
model : firstModel //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
window.AppState.SelectedModelId = selectedModel.id;
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
}
EDIT
Handling selected class (highlight) in list view . see comments for reference.
list view template -
<ul>
<% _.each(collection, function(item, index){ %>
<li data-id='<%= item.id %>'><%= item.name %></li>
<% }); %>
</ul>
inside list view add following method -
changeSelectedHighlight : function(id){
this.$(li).removeClass('selected');
this.$("[data-id='" + id + "']").addClass('selected');
}
simply call this method from updateSelection method and during PageView initialize.
this.list.changeSelectedHighlight(firstModel.id);
My code gets the fileType and start new ImageView accordingly.
var frameIdRegion = new Marionette.Region({
el: '#viewerId'
});
// Create a new Backbone Marionette View with item model. Render View and Show it on Screen.
if(fileType === 'Image'){
frameIdRegion.close(imageView);
frameIdRegion.reset();
var imageView = new ImageView({model: item});
frameIdRegion.show(imageView);
$(".frameId img").show();
$(".frameId img").load(function(){
$("#fit_to_screen").trigger('click');
});
imageView.close();
}
else if(fileType === 'pdf'){
var objectView = new ObjectView({model: item});
frameIdRegion.reset();
frameIdRegion.show(objectView);
$("#pdf_viewer").show();
$('#pdf_viewer').trigger('focus');
}
else if(fileType === 'unsupported'){
var errorView = new ErrorView({model: item});
frameIdRegion.reset();
frameIdRegion.show(errorView);
}
But my Old instance of ImageView is not getting cleared when i start a new ImageView. And my events are occuring many times. This is my ImageView code.
var ImageView = Marionette.ItemView.extend({
template: template,
className: 'frameId',
onClose: function(){
this.remove();
this.unbind();
this.model.unbind("change", this.modelChanged);
}
});
Please help what i am doing wrong?
try using backbones listenTo function. It gets attached to whatever is calling it, so when your ItemView gets disposed then so will the event listener.
http://backbonejs.org/#Events-listenTo
It also has a stopListening method if you really need it. When your view is removed, it should automatically stop listening to everything that was called by listenTo
http://backbonejs.org/#View-remove
I have an edit view for a Backbone Model that I create each time the the element is clicked on. The problem I have is that the edit view needs two Backbone collections to create the edit form (it contains two <select> lists).
The view:
MyApp.elementView = Backbone.View.extend({
events: {
'click .edit': 'editForm',
},
editForm: function(ev) {
var editView = new TimeTrack.Views.EditJob({
model: this.model
// This view needs two more collections
// for the <select> elements
});
...
}
});
Instantiate the view:
var elementView = new MyApp.elementView({
collection: elementCollection
});
What is the best way to push the needed collections to the edit view? Do I have to pass the collections need for the edit view from the elementView form the instantiation? Or is there a better way of doing this?
I did so, passed in view 2 collections, 1 - the main and the other as follows:
to elementView - second collection and in elementView recive her.
example:
in router I'm
initialize: ->
(YourNameSpace).secondCollection = new (YourNameSpace).secondCollection
elements: =>
view = new (YourNameSpace).elementView( secondCollection: #secondCollection )
$('.l-yield').html(view.render().el)
My Code:
I am new to Backbone.js and trying to build an app with Backbone.js and PHP. When I am trying to call add in the router, I am getting error:
Uncaught TypeError: Object [object Object] has no method 'set'.
Please help me to find my mistake.
Thanks.
// Models
window.Users = Backbone.Model.extend({
urlRoot:"./bb-api/users",
defaults:{
"id":null,
"name":"",
"email":"",
"designation":""
}
});
window.UsersCollection = Backbone.Collection.extend({
model:Users,
url:"./bb-api/users"
});
// Views
window.AddUserView = Backbone.View.extend({
template:_.template($('#new-user-tpl').html()),
initialize:function(){
this.model.bind("click", this.render, this);
},
render:function(){
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events:{
"click .add":"saveUser"
},
saveUser:function(){ alert('saveUser');
this.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(this.model.isNew()){
this.model.create(this.model);
}
return false;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"welcome",
"users":"list",
"users/:id":"userDetails",
"add":"addUser"
},
addUser:function(){
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
$('#content').html(this.addUserView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
As suggested in the comments, the problem starts here here:
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
and finishes here:
saveUser:function(){ alert('saveUser');
this.model.set({
By passing a collection in place of a model you create confusion, and as a result later in the saveUser function you try to call a Backbone.Model method (set) on a Backbone.Collection instance.
Note: As of version 1.0.0 Backbone.Collection now has a set method. In previous versions, such as the one used by the question's author, that method was instead called update.
There are several steps you can take to clarify this code. For starters, I would rename your model and collection classes so that it's clear that the model is the singular form and the collection is the plural form:
window.Users => window.User
window.UsersCollection => window.Users
Next, I would create a new User model, instead of a Users collection, and pass that to your view:
this.addUserModel = new User();
this.addUserView = new AddUserView({model:this.addUserModel});
Finally, I'd remove these lines:
if(this.model.isNew()){
this.model.create(this.model);
}
For one thing, the model will always be new (as you just created it before passing it in), but more importantly you don't need to call the Collection's create method because that method creates a new model, when you already have one created. Perhaps what you should add instead is :
this.model.save();
if your intent is to save the model to your server.
Since you already specified a urlRoot for the model, that should be all you need to create a new model, pass it to your view, have your view fill in its attributes based on DOM elements, and finally save that model's attributes to your server.
I think you are facing problem with object scope. When event fired it send to event object to that function. Just try this it may work
Declare global variable with the current view inside the initialize
initialize : function(){ self = this; }
then change this to self,
saveUser:function(){ alert('saveUser');
self.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(self.model.isNew()){
self.model.create(this.model);
}
return false;
}
I render a collection of models, which is associated with a collectionView where when rendered each element in the collection has its own 'itemview' which is rendered.
When a collection is sorted and the listView re-rendered based on the new order, I had been creating a totally new view for each item, and as I was not clearing up any previous instances of views associated with that model, I believe zombies being left around.
So initially rendering my collection I would do...
render : function() {
$(this.el).empty();
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view = new TB_BB.RequestItemView({model : model});
els.push(view.render().el);
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So whenever the render function was called a second/third etc time, I had not cleared up the TB_BB.RequestItemView (hence the zombies)
To overcome this I tried to add some simple caching in the collections view, so that instead of creating a new itemview if it had already been created use that instead. My code
initialize : function(){
_.bindAll(this,"render");
this.collection.bind("add", this.render);
this.collection.bind("remove", this.render);
this.template = $("#request-list-template");
this.views = {};
},
events : {
"change #sort" : "changesort",
"click #add-offer" : "addoffer",
"click #alert-button" : "addalert"
},
render : function() {
$(this.el).empty();
outerthis = this;
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view;
if(outerthis.views[model.get('id')]) {
view = outerthis.views[model.get('id')];
} else {
view = new TB_BB.RequestItemView({model : model});
outerthis.views[model.get('id')] = view;
}
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So this works in so much as the views are re-used - however what I have noticed is that if I use a cached view (e.g. the collection has been sorted and the render function finds a cached view) that all of the events on the sub itemview stop working? why is that?
Also could anyone suggest a better way of doing this?
You can use delegateEvents ( http://documentcloud.github.com/backbone/#View-delegateEvents ) to bind the events again.
As OlliM mentioned the reason is because the events are bound to the dom element, but instead of rebinding the element you can also just detach them instead of removing them (detach keeps the event bindings http://api.jquery.com/detach/)
something like
var $sortContainer = $('#sorting-container');
$('li', $sortContainer).detach();
And then just reattach the element
$cnt.append(view.el);
I would also consider using a document fragment while rebuilding/sorting the list and then attaching appending that instead.