Backbone: Lost reference to actual view? - javascript

I'm new to Backbone (and Marionette), and trying to write a pretty simple app using both. The app has a menu of "groups" on the left nav, and a list of "entries" on the main right div. Every time a Group menu item is clicked, I filter the entries with the group ID and show them, when hide all others.
Here is the Entry Item view (all scripts are in CoffeeScript btw):
class EntryItemView extends Backbone.Marionette.ItemView
tagName: 'tr'
template: _.template $('#entryItemTemplate').html()
render: ->
#$el.html #template(#model.toJSON())
show: ->
#$el.show()
hide: ->
#$el.hide()
Here is the Entry List view, extending Marionette's CollectionView:
class EntryListView extends Backbone.Marionette.CollectionView
itemView: EntryItemView
el: '#main tbody'
This is the AppRouter, pretty much straightforwad:
class AppRouter extends Backbone.Router
routes:
'group/:id' : 'showGroup'
router = new AppRouter()
router.on 'route:showGroup', (id) ->
_.each entryViews, (view) ->
if view.model.get('group_id') is parseInt(id)
view.show()
else
view.hide()
(The entryViews variable is a simple global array to store all EntryItemView instances).
With this approach, navigating the app to /group/:id indeed invokes the show() and hide() method of each EntryItemView object. The problem is, looks like the reference between this object and the actual HTML doesn't exist, so the actual element <tr> doesn't show or hide.
Can you guys point out what I'm doing wrong here? Thanks in advance.

Here are a couple pointers:
since your template is in the HTML, you just specify the jQuery selector with template: "#entryItemTemplate"
you can remove the render declaration, because Marionette does that on its own (i.e. you're implementing the default behavior)
unless you know what you're doing, you typically don't declare an el property in a collection view. Instead you declare a region (possibly within a layout), where you will call the show method to display a view instance
The reason your code probably doesn't work is that it looks like Backbone code with some Marionette stuff thrown in. Take a look at the free sample to my book on Marionette. It should get you started quickly with Marionette and will explain most of what you're trying to accomplish here.

Related

How to check if a backbone view is rendered?

I am trying to append a view to an item in Backbone with a following code:
var viewContainer = this.$el.find('.view-container'),
pageWrap = this.$el.nextAll();
FIX
if (viewContainer.empty()) {
this.myView= new ProductsView();
viewContainer.append(application.myView.render().$el),
console.log(myView);
}
I am appending this view to the viewContainer with a toggle function, however, every time I click on the button, myView is appended again and again to the viewContainer instead of of only once. How do I check if the view is already rendered inside it before appending it? Is there a !this.rendered() equivalent I can use?
I found this thread but it is not helping me in this instance.
UPDATE - FROM console.log(viewContainer)
[div.view-container.product-container.active, div#subjects_menu.view-container.product-container.hidden.active, prevObject: p.fn.p.init[1], context: undefined, selector: ".view-container"]
From the looks of it, you want to make sure ProductsView is not created if it already exists.
Simplest way to do this would be:
if(!this.myView) {
this.myView= new ProductsView();
viewContainer.append(application.myView.render().$el),
}
It is better to work with application state than querying DOM. When you remove product view, simply do this.myView = null afterwards.
The only time you'd want to query DOM to know if a view is rendered is probably when you have to integrate an isolated external application over which you have no control that doesn't trigger any event/provide callbacks etc while rendering.

How to reference ember.js controllers that are in nested folders?

I'm building an EventController that has little modules of logic within sections or div's of the event screen.
Say for instance, event details may be in the main Event template but a small section might be the user's status with the event such as whether they RSVP'd etc. but since it's controlled by a different model than the event I'd think it should have it's own controller.
Would I put this in the EventController like such:
Controller = BaseController.extend
needs: ['event/user-status-area']
userStatusArea: Ember.computed.alias("controllers.event.user-status-area")
This obviously isn't working otherwise I wouldn't be here... but I'm looking for suggestions.
I'm very new to ember / ember-cli concepts so I'm sorry if I'm just blatantly way off base here.
In my brain, I would imagine keeping everything about an event centralized under the one EventController...
Am I missing something big? Is this possibly where the "Router" comes in?
UPDATE:
If so, I'd imagine it might look something like this in Router:
Route = BaseRoute.extend
model: (params) ->
#store.find('event',params.id)
renderTemplate: (controller,model) ->
userStatusController = controller.get('userStatusArea')
#render 'event'
#render 'event/user-status-area',
into: 'event',
outlet: 'user-status-area',
controller:userStatusController
model: model.event_user.find(#get('session.current_user.userId'))
No idea if this would even be considered a best practice for ember?
I guess this would be the question... what is the best way to create this type of structure?
One way is to create a nested route:
router.js
this.resource('event',{path:'event/:id'}, function(){
this.route('userStatus');
})
in the event template, you would add an {{outlet}}
When you transition to event/{id}/userStatus, the outlet would be automatically rendered with the templates/event/user-status.hbs template.
When you reference controllers/views etc. in ember-cli with a filename e.g. user-status, you need to reference it in camelCase:
needs: ['event/userStatus'],
not user-status.
Hope this helps.

Why isn't my Backbone Collection reset event firing?

I went through the Railscast tutorial and got it all working. Working on a quick prototype to see if Backbone is viable but I've messed something up and I'm not sure what I've done wrong. I'm on Backbone 1.
View
class Shsh.Views.AssetsIndex extends Backbone.View
template: JST['assets/index']
initalize: ->
#collection.on('reset', #render, this)
render: ->
$(#el).html(#template(assets: #collection))
console.log('rendered')
this
Router
class Shsh.Routers.Assets extends Backbone.Router
routes:
'': 'index'
initialize: ->
#collection = new Shsh.Collections.Assets()
#collection.fetch({reset: true})
index: ->
view = new Shsh.Views.AssetsIndex(collection: #collection)
$('#container').html(view.render().el)
The view gets rendered fine, but the length of #assets comes back as 0. I can go through the steps in the console and when I render the view again it comes back as being the correct length. What am I doing wrong?
EDIT:
I also do actually have a collection and model. The code there is all boilerplate generated by Backbone On Rails.
You are calling fetch() too early -- in router creation. Should be called in specific route code instead. The way you implemented it, fetch and reset may complete before route is triggered and therefore you'll start listening to reset after it has been fired
I'm an idiot. Initialize is spelt wrong in Shsh.Views.AssetsIndex.

How to handle initializing and rendering subviews in Backbone.js?

I have three different ways to initialize and render a view and its subviews, and each one of them has different problems. I'm curious to know if there is a better way that solves all of the problems:
Scenario One:
Initialize the children in the parent's initialize function. This way, not everything gets stuck in render so that there is less blocking on rendering.
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.render().appendTo(this.$('.container-placeholder');
}
The problems:
The biggest problem is that calling render on the parent for a second time will remove all of the childs event bindings. (This is because of how jQuery's $.html() works.) This could be mitigated by calling this.child.delegateEvents().render().appendTo(this.$el); instead, but then the first, and the most often case, you're doing more work unnecessarily.
By appending the children, you force the render function to have knowledge of the parents DOM structure so that you get the ordering you want. Which means changing a template might require updating a view's render function.
Scenario Two:
Initialize the children in the parent's initialize() still, but instead of appending, use setElement().delegateEvents() to set the child to an element in the parents template.
initialize : function () {
//parent init stuff
this.child = new Child();
},
render : function () {
this.$el.html(this.template());
this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}
Problems:
This makes the delegateEvents() necessary now, which is a slight negative over it only being necessary on subsequent calls in the first scenario.
Scenario Three:
Initialize the children in the parent's render() method instead.
initialize : function () {
//parent init stuff
},
render : function () {
this.$el.html(this.template());
this.child = new Child();
this.child.appendTo($.('.container-placeholder').render();
}
Problems:
This means that the render function now has to be tied down with all of the initialization logic as well.
If I edit the state of one of the child views, and then call render on the parent, a completely new child will be made and all of its current state will be lost. Which also seems like it could get dicey for memory leaks.
Really curious to get your guys' take on this. Which scenario would you use? or is there a fourth magical one that solves all of these problems?
Have you ever kept track of a rendered state for a View? Say a renderedBefore flag? Seems really janky.
This is a great question. Backbone is great because of the lack of assumptions it makes, but it does mean you have to (decide how to) implement things like this yourself. After looking through my own stuff, I find that I (kind of) use a mix of scenario 1 and scenario 2. I don't think a 4th magical scenario exists because, simply enough, everything you do in scenario 1 & 2 must be done.
I think it'd be easiest to explain how I like to handle it with an example. Say I have this simple page broken into the specified views:
Say the HTML is, after being rendered, something like this:
<div id="parent">
<div id="name">Person: Kevin Peel</div>
<div id="info">
First name: <span class="first_name">Kevin</span><br />
Last name: <span class="last_name">Peel</span><br />
</div>
<div>Phone Numbers:</div>
<div id="phone_numbers">
<div>#1: 123-456-7890</div>
<div>#2: 456-789-0123</div>
</div>
</div>
Hopefully it's pretty obvious how the HTML matches up with the diagram.
The ParentView holds 2 child views, InfoView and PhoneListView as well as a few extra divs, one of which, #name, needs to be set at some point. PhoneListView holds child views of its own, an array of PhoneView entries.
So on to your actual question. I handle initialization and rendering differently based on the view type. I break my views into two types, Parent views and Child views.
The difference between them is simple, Parent views hold child views while Child views do not. So in my example, ParentView and PhoneListView are Parent views, while InfoView and the PhoneView entries are Child views.
Like I mentioned before, the biggest difference between these two categories is when they're allowed to render. In a perfect world, I want Parent views to only ever render once. It is up to their child views to handle any re-rendering when the model(s) change. Child views, on the other hand, I allow to re-render anytime they need since they don't have any other views relying upon them.
In a little more detail, for Parent views I like my initialize functions to do a few things:
Initialize my own view
Render my own view
Create and initialize any child views.
Assign each child view an element within my view (e.g. the InfoView would be assigned #info).
Step 1 is pretty self explanatory.
Step 2, the rendering, is done so that any elements the child views rely on already exist before I try to assign them. By doing this, I know all child events will be correctly set, and I can re-render their blocks as many times as I want without worrying about having to re-delegate anything. I do not actually render any child views here, I allow them to do that within their own initialization.
Steps 3 and 4 are actually handled at the same time as I pass el in while creating the child view. I like to pass an element in here as I feel the parent should determine where in its own view the child is allowed to put its content.
For rendering, I try to keep it pretty simple for Parent views. I want the render function to do nothing more than render the parent view. No event delegation, no rendering of child views, nothing. Just a simple render.
Sometimes this doesn't always work though. For instance in my example above, the #name element will need to be updated any time the name within the model changes. However, this block is part of the ParentView template and not handled by a dedicated Child view, so I work around that. I will create some sort of subRender function that only replaces the content of the #name element, and not have to trash the whole #parent element. This may seem like a hack, but I've really found it works better than having to worry about re-rendering the whole DOM and reattaching elements and such. If I really wanted to make it clean, I'd create a new Child view (similar to the InfoView) that would handle the #name block.
Now for Child views, the initialization is pretty similar to Parent views, just without the creation of any further Child views. So:
Initialize my view
Setup binds listening for any changes to the model I care about
Render my view
Child view rendering is also very simple, just render and set the content of my el. Again, no messing with delegation or anything like that.
Here is some example code of what my ParentView may look like:
var ParentView = Backbone.View.extend({
el: "#parent",
initialize: function() {
// Step 1, (init) I want to know anytime the name changes
this.model.bind("change:first_name", this.subRender, this);
this.model.bind("change:last_name", this.subRender, this);
// Step 2, render my own view
this.render();
// Step 3/4, create the children and assign elements
this.infoView = new InfoView({el: "#info", model: this.model});
this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
},
render: function() {
// Render my template
this.$el.html(this.template());
// Render the name
this.subRender();
},
subRender: function() {
// Set our name block and only our name block
$("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
}
});
You can see my implementation of subRender here. By having changes bound to subRender instead of render, I don't have to worry about blasting away and rebuilding the whole block.
Here's example code for the InfoView block:
var InfoView = Backbone.View.extend({
initialize: function() {
// I want to re-render on changes
this.model.bind("change", this.render, this);
// Render
this.render();
},
render: function() {
// Just render my template
this.$el.html(this.template());
}
});
The binds are the important part here. By binding to my model, I never have to worry about manually calling render myself. If the model changes, this block will re-render itself without affecting any other views.
The PhoneListView will be similar to the ParentView, you'll just need a little more logic in both your initialization and render functions to handle collections. How you handle the collection is really up to you, but you'll at least need to be listening to the collection events and deciding how you want to render (append/remove, or just re-render the whole block). I personally like to append new views and remove old ones, not re-render the whole view.
The PhoneView will be almost identical to the InfoView, only listening to the model changes it cares about.
Hopefully this has helped a little, please let me know if anything is confusing or not detailed enough.
I'm not sure if this directly answers your question, but I think it's relevant:
http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/
The context in which I set up this article is different, of course, but I think the two solutions I offer, along with the pros and cons of each, should get you moving in the right direction.
To me it does not seem like the worst idea in the world to differentiate between the intital setup and subsequent setups of your views via some sort of flag. To make this clean and easy the flag should be added to your very own View which should extend the Backbone (Base) View.
Same as Derick I am not completely sure if this directly answers your question but I think it might be at least worth mentioning in this context.
Also see: Use of an Eventbus in Backbone
Kevin Peel gives a great answer - here's my tl;dr version:
initialize : function () {
//parent init stuff
this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!
this.child = new Child();
},
I'm trying to avoid coupling between views like these. There are two ways I usually do:
Use a router
Basically, you let your router function initialize parent and child view. So the view has no knowledge of each other, but the router handles it all.
Passing the same el to both views
this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});
Both have knowledge of the same DOM, and you can order them anyway you want.
What I do is giving each children an identity (which Backbone has already done that for you: cid)
When Container does the rendering, using the 'cid' and 'tagName' generate a placeholder for every child, so in template the children has no idea about where it will be put by the Container.
<tagName id='cid'></tagName>
than you can using
Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();
no specified placeholder is needed, and Container only generate the placeholder rather than the children's DOM structure. Cotainer and Children are still generating own DOM elements and only once.
Here is a light weight mixin for creating and rendering subviews, which I think addresses all the issues in this thread:
https://github.com/rotundasoftware/backbone.subviews
The approach taken by this plug is create and render subviews after the first time the parent view is rendered. Then, on subsequent renders of the parent view, $.detach the subview elements, re-render the parent, then insert the subview elements in the appropriate places and re-render them. This way subviews objects are reused on subsequent renders, and there is no need to re-delegate events.
Note that the case of a collection view (where each model in the collection is represented with one subview) is quite different and merits its own discussion / solution I think. Best general solution I am aware of to that case is the CollectionView in Marionette.
EDIT: For the collection view case, you may also want to check out this more UI focused implementation, if you need selection of models based on clicks and / or dragging and dropping for reordering.

backbone.js collection not properly removing item in view

I'm having some trouble deleting an item from a collection inside a model when in a view. Basically the model/collection structure is the following:
Basically when I try to remove an item from the sub item collection in the sub item view it actually removes the correct item from the collection. However when I come to persisting the main model the item seems to be still in the collection.
This is the how my views are structured:
The main view inserts the DOM nodes required by the main model, and them main model creates a new view for the item model etc. All views are getting the main model as model option like so:
new App.Views.MainModelView({
model : this.model,
el : $('#nodeID')
})
The only difference is in the creation of the Sub-item model view, where, due to re usability of the view and template, I still pass in the main model, however I also pass in the item in the item collection that is currently being modified. Which looks like this:
new App.Views.ItemView({
model : this.model,
item : this.selectedItem,
el : $('#nodeID')
});
In the sub-item's view init method I do the following:
this.item = (this.options.item) ? this.options.item : this.model;
To remove a sub-item from the sub-item collection I do:
removeSubItem : function(e) {
// get the id of the sub-item to be removed
var id = $(e.target).closest('tr').attr('data-id');
if (!id) throw "Could not retrieve data id";
// retrieve the sub-item from the collection
var subItem = this.item.subItems.get(id);
// remove the sub-item from the collection
this.item.subItems.remove(subItem);
},
As I said earlier when I remove the sub-item and I inspect the collection modified by the view I can see that the sub-item has been removed from the collection, however then I persist the main model the removed sub-item re-appears. The leads me to believe that somewhere along the line the sub-item collection might be cloned which could explain the sudden reappearance of the sub-item.
I know this is a fairly specific problem and I'm not sure if it is possible to get to the cause of the problem with what I have provided here, if you need any more information please let me know.
Thanks for all your help,
Vincent
========== EDIT ============
To answer some of the questions below let me outline the scope in which I am experiencing this issue:
If I console log the this.item.subItems collection in the SubItem view, after removeSubItem was called, I can see that the instance of the SubItem model has been removed successfully.
Before I call the save method on the main model I console log the return of the toJSON function. At this point I am experiencing the problem that the previously removed instance is 'back' in the collection. I have been monitoring the traffic between client and server with both Wireshark and Google chrome's developer console and there is no call to the server to refresh any of the models.
The toJSON method for the SubItem collection looks like this:
toJSON : function() {
App.log(["SubItem::collection::toJSON", this], "info");
var json = {};
// make sure the key for each SubItem is the primary key
this.each(function(subItem) {
json[subItem.get('id')] = subItem.toJSON();
});
return json;
}
Backbone.js support for nested collection/models is non-existent, and they provide no saving support (see http://documentcloud.github.com/backbone/#FAQ-nested). You have to override toJSON on any model with a subcollection. I've run into this scenario a million times. If you have something like (in coffeescript):
class MainModel extends Backbone.Model
itemCollection: ->
#_itemCollection ?= new ItemCollection(#get('itemCollection'))
class ItemCollection extends Backbone.Collection
model: ItemModel
class ItemModel extends Backbone.Model
subCollection: ->
#_subCollection ?= new SubCollection(#get('subCollection'))
class SubCollection extends Backbone.Collection
model: SubModel
class SubModel extends Backbone.Model
mainModel = new MainModel(json)
Then in order for mainModel.save() to work, you need to override toJSON on MainModel and ItemModel, like:
class MainModel extends Backbone.Model
itemCollection: ->
#_itemCollection ?= new ItemCollection(#get('itemCollection'))
toJSON: ->
return _.extend(#attributes, {itemCollection: #itemCollection().toJSON()})
class ItemModel extends Backbone.Model
subCollection: ->
#_subCollection ?= new SubCollection(#get('subCollection'))
toJSON: ->
return _.extend(#attributes, {subCollection: #subCollection().toJSON()})
I wrote the example in coffeescript because it's much more concise than javascript. If you need any help making sense of it, please just ask.
Hope this helps!
--- Note ---
Technically, in coffeescript, the toJSON methods could simply be:
toJSON: ->
_.extend #attributes, itemCollection: #itemCollection().toJSON()
But I wrote it the way I did to be more understandable to non-coffeescripters.
Without looking at your whole code base I think you might have your structure slightly wrong. Normally with backbone I hardly ever if ever pass the :el element directly into the view. The view is responsible for generating it's own el. After it has been rendered I then insert the new view.el into the DOM. Like below
var subView = new FooView({ model: fooModel });
mainView.$(".list").append(subView.el);
In the above case there is a backbone object with every subview. If you need to
remove the subview you don't need to do a selector query to find it, you just call
the remove method on the object and it knows how to remove itself from the dom.
Or to be more specific the subView handles a click event on itself which it
can then handle by destroying it's associated model and then calling remove on itself

Categories

Resources