I know that there are a few questions around regarding this, but the answers are not very clear for me to implement. That's why I'm asking this question again so I can have a clear and simple answer.
I've always had trouble with Collection in Backbone, especially populating it with JSON data.
I can't seem to get the collection to render in the View, even though in firebug I can see that, it's being fetched from the server, but the screen is still empty.
Also, when I do a console.log('callers: ', this.callerList), it returns an object with models=[0]. But when I expand the object, models is full of data from the JSON file. What's going on with Backbone and it's confusing results?
Can someone please explain to me how to do it? I've been battling this for ages and I can't get my head around it.
Many Thanks
JS:
(function($, window) {
// model
var CallerModel = Backbone.Model.extend({});
// collection
var CallersList = Backbone.Collection.extend({
model: CallerModel,
url: 'js/json/callers.json'
});
// view
var CallerView = Backbone.View.extend({
el: '.caller-app',
template: _.template($('#callers-template').html()),
initialize: function() {
this.callerList = new CallersList();
this.callerList.fetch();
this.callerList.bind('reset', this.render);
console.log('caller: ', this.callerList);
},
render: function(e) {
console.log('RENDER');
_.each(this.collection.models, function(caller) {
this.$el.append(this.template(caller.toJSON()));
console.log('callerList: ', caller);
}, this);
return this;
}
});
// start
var callerView = new CallerView();
}(jQuery, window));
HTML:
<!-- wrapper -->
<div class="wrapper">
<h1>Missed Calls App</h1>
<div class="caller-app"></div>
</div>
<!-- wrapper -->
<!-- templates -->
<script type="text/template" id="callers-template">
<div class="caller">
<h2><%= title %> <%= name %> called</h2>
<h3>From <%= agency %></h3>
<p>When: <%= when %></p>
<p>Contact: <%= tel %></p>
<p>Says:"<%= message %>"</p>
</div>
</script>
<!-- templates -->
JSON:
[
{
"id": 1,
"title": "Mrs",
"name": "Mui",
"agency": "Ryuzanpaku Dojo",
"when": "evening",
"tel": "0207 123 45 67",
"message": "Check your availability"
},
{
"id": 2,
"title": "Mrs",
"name": "Shigure",
"agency": "Ryuzanpaku Dojo",
"when": "evening",
"tel": "0207 123 45 67",
"message": "Check your availability"
}
]
You haven't actaully assigned a collection to your CallerView, in addition when you iterate though the collection you should be using this.collection.models instead of this.model.models
For example when initializing you caller list
initialize: function() {
initialize: function() {
this.collection = new CallersList();
this.collection.fetch();
this.collection.bind('reset', this.render);
}
And when rendering
render: function(e) {
_.each(this.collection.models, function(caller) {
this.$el.append(this.template(caller.toJSON()));
}, this);
return this;
}
Here's a link to a jsbin
Some additional points
In general you want to decouple your code as much as possible. To this end it is probably better to declare and initialize your collection outside of your view and then pass it in. This also has the advantage of making your code more reusable, for example say you wanted to render a second list of calls (let say recent calls), you can now just create a second instance of your view passing in a collection and element.
For example
var missedCalls = new CallersList();
var callerView = new CallerView({collection : missedCalls, el: '#missedCalls' });
missedCalls.fetch();
var recentCalls = new CallerList(); //you probably want to use a different url
var recentCallersView = new CallerView({collection : recentCalls, el:'#recentCalls'});
recentCalls.fetch();
Another point worth mentioning, currently you are rendering all items in your collection for each fetch, including any that have been already rendered. You might want either empty your el before rendering or listen to the add event instead and render each item individually as it's added. In addition it's worth pointing out that fetch isn't really meant to be used to load data on page load, from the documentation
Note that fetch should not be used to populate collections on page
load — all models needed at load time should already be bootstrapped
in to place. fetch is intended for lazily-loading models for
interfaces that are not needed immediately: for example, documents
with collections of notes that may be toggled open and closed.
Related
I'm trying to get my head around switching views / passing views to another view.
I have an app that is calling in a kimono API, that's all setup with the supersonic background and looks fine. I have 1 string and 2 objects in the API. I have a page that is calling in the full list of events using a page called event:
{{ event.eventdescription }}
The Event#Index controller is:
angular
.module('event')
.controller("IndexController", function ($scope, Event, supersonic) {
$scope.events = null;
$scope.showSpinner = true;
Event.all().whenChanged( function (events) {
$scope.$apply( function () {
$scope.events = events;
$scope.showSpinner = false;
});
});
});
And all of that displays properly. The issue is when I click on one of those items shown which should go to the specific event I get nothing. And I'm sure I'm doing this wrong or don't understand enough about switching views. I've read many examples, but I'm not getting how it all goes together.
here is my event#show page. Very generic just trying to load any information at this point.
<div ng-controller="ShowController">
<super-navbar>
<super-navbar-title>
Show
</super-navbar-title>
</super-navbar>
<div class="padding">
{{ event.eventdescription }}
</div>
</div>
And the showcontroller:
angular
.module('event')
.controller("ShowController", function ($scope, Event, supersonic) {
$scope.events = null;
Event.all().whenChanged( function (events) {
$scope.$apply( function () {
});
});
});
And this always returns a blank page. When i check the log it says Undefined.undefined which i'm not sure what that means.
Any insight on this is greatly appreciated. In the appgyver docs I saw something called.
var view = new supersonic.ui.View("bananas#show");
supersonic.ui.layers.push(view);
But I'm not sure how to use this?
ANY insight is appreciated.
So, UPDATED I have:
here's the event#index i'm working with.
<div ng-controller="IndexController">
<super-navbar>
<super-navbar-title>
Event Index
</super-navbar-title>
</super-navbar>
<ul class="list" ng-hide="events.length == 0">
<super-navigate view-id="event#show" data-params-id="{{event.id}}" ng-repeat="event in events">
<li class="item item-icon-right">
<h2 ng-bind="event.EventTitles['text']"></h2>
<img ng-src="{{ event.HeadlineImages.src }}" width="100px" height="100px">
<p> {{ event.eventdescription }} </p>
<i class="icon super-ios7-arrow-right"></i>
</li>
</super-navigate>
</ul>
</div>
And the Index Controller
angular
.module('event')
.controller("IndexController", function ($scope, Event, supersonic) {
$scope.events = null;
Event.all().whenChanged( function (events) {
$scope.$apply( function () {
$scope.events = events;
});
});
});
The show html page.
<div ng-controller="ShowController">
<super-navbar>
<super-navbar-title>
Show
</super-navbar-title>
</super-navbar>
<div class="padding">
<p>
{{event.eventdescription}}
</p>
</div>
</div>
The ShowController
angular
.module('event')
.controller("ShowController", function ($scope, Event, supersonic) {
supersonic.ui.views.current.params.onValue( function (Event) {
$scope.events = event.id;
});
Event.find($scope.events).then( function (Event) {
$scope.$apply( function () {
$scope.event = Event;
});
});
});
And I also updated the structure.coffee as so
rootView:
location: "event#index"
preloads: [
{
id: "event#show"
}
{
id: "using-the-scanner"
location: "example#using-the-scanner"
}
]
Any help is appreciated.
It doesn't look like the data is being set in the your ShowController. I commented about this before. I think you need to pass the id of the event using <super-navigate> with a location property and a data-params-id or whatever you want the parameter name to be. Then in your ShowController you can access it with:
supersonic.ui.views.current.params.onValue( function (values) {
// values.nameOfPropertyPassedInCouldBeEventId
$scope.id = values.id;
});
Then you might be able to do something like this to access the Event by id:
Event.find($scope.id).then( function (theEvent) {
$scope.$apply( function () {
$scope.event = theEvent;
});
});
Now in your view where you have {{ event.eventdescription }} there should be some data.
And another piece for when the view is visible meaning every time you see that view page this will fire:
supersonic.ui.views.current.whenVisible( function () {
// your code for watching events
});
Ok, after a couple weeks of trying to get this working and although, I still haven't been able to get this to work yet.. I think I'm getting somewhere with this FINALLY... It seems the biggest problem here is using Kimono and AppGyver. The JSON file has been updated in Kimono using:
function transform(data) {
data.results.collection1 = data.results.collection1.map(function(o) {
o.eventdescription = {
text: o.eventdescription
}
return o;
});
return data;
}
This cleans up the JSON file exported/ coming in as API to App Gyver so that all parts are objects. ( I know, maybe not a big deal, but I just wanted to make this as clean as possible). To give you an idea of the before and after of using this script in the Kimono Modify Results box -->
BEFORE:
"EventTitles": {
"href": "http://",
"src": "http://.jpg",
"text": "Lorem Ipsum"
},
"HeadlineImages": {
"href": "http://",
"src": "http://.jpg",
"text": "Lorem Ipsum"
},
"eventdescription":"Lorem Ipsum"
},
which leaves eventdescription as a string rather than object and then the AFTER:
"EventTitles": {
"href": "http://",
"src": "http://.jpg",
"text": "TEXT"
},
"HeadlineImages": {
"href": "http://",
"src": "http://.jpg",
"text": "TEXT"
},
"eventdescription": {
"text": "TEXT"
},
So, after running this into Kimono as you can see all entries are "objects". And you'd use &kimmodify=1 AFTER the apikey in the link thusly:
https://www.kimonolabs.com/api/{indentifier}{apikey}&kimmodify=1
NEXT, as I was explained to by the AppGyver community one would pretty much need an "id" of sorts for each item in the JSON / API that's being created to be able to use the ShowController to create a reasonable/ feasible url string on the show.html.
Which should create something like /app/tier/showid=123456789 when going from the index to a specific entry view.
(You find the URLs by using the debug mode in AppGyver either via Safari Web Inspector on Mac with the IOS Emulator. or a browser using http://localhost:[some port number]/location/of/app when using the Android Emulator (the recommended Genymotion).
So, to do this, in Kimono use the API Hash addition &kimhash=1 to the end of your url AFTER the APIKEY but BEFORE the modify such as this:
https://www.kimonolabs.com/api/{indentifier}{apikey}&kimhash=1&kimmodify=1
. See: Kimono API Docs- Re:Hash.
This creates something like
"EventTitles": {
"href": "http://",
"src": "http://.jpg",
"text": "TEXT"
},
"HeadlineImages": {
"href": "http://",
"src": "http://.jpg",
"text": "TEXT"
},
"eventdescription": {
"text": "TEXT"
},
"hash":"1a2b3c4d5e6f7g8h9z"},
a random 'indentifier' is created for each entry.
Now, that's where I'm stuck now. ...because the API URL needing to come in is:
https://www.kimonolabs.com/api/{indentifier}{apikey}&kimhash=1&kimmodify=1
and when you go to configure your API on the backend there is no area I see to enter this new &kimhash=1&kimmodify=1 that needs to be at the end of the URL to call in the correctly formatted and id'd API and as far as I can see there is no reference for doing this.
http://docs.appgyver.com/supersonic/guides/data/other-data-providers/kimono-labs/
I feel like this is the next to last step in figuring this all out and finally being able to get this up and working. The last being to obviously revisit pulling in the id to the ShowController which I'm feeling somewhat confident about if I can somehow figure out this last part.
Any ideas??
I'm a noob in backbone.js and JavaScript for that matter... and I'm trying to build a simple widget system with Jquery and backbone.js, but I can't seem to figure out how to get multiple instances of my view to render. I am, however able to get one instance to render... my ultimate goal is to build a system where i can click on a button and have it render a new widget on the screen each time.
here is my code:
<script type="text/template" id="widget-template">
<div class="widget-wrap">
<div class="widget-header">
<span class="widget-title"><%= widgetInfo.get('title') %></span>
<span class="widget-close"></span>
<span class="widget-hide"></span>
<span class="widget-expand"></span>
</div>
<div class="widget-container">
<!-- this is where the widget content goes -->
</div>
</div>
</script>
<script typ="text/javascript">
var baseWidget = Backbone.Model.extend({
defaults: {
title: "Base",
author: "AB",
multipleInstances: false,
description: "This is the base widget",
pathToIcon: "",
state: "Open",
position: {left:0, top:0}
}
});
var widgetCollection = Backbone.Collection.extend({
model: baseWidget
});
var widgetcol = new widgetCollection();
var baseView = Backbone.View.extend({
el: '.wraper',
render: function(pos = {left:0, top:0}) {
var widget = new baseWidget();
widgetcol.add(widget);
console.log(widgetcol.length);
widget.set({'position':pos})
var template = _.template($('#widget-template').html(), {widgetInfo: widget});
this.$el.html(template);
this.$el.offset({top:widget.get('position').top, left:widget.get('position').left})
$('.widget-wrap').draggable({
handle: ".widget-header",
stop: function (event, ui) {
widget.set({position: ui.position});
console.log(widget.get('position'));
}
});
}
});
BV = new baseView();
BV.render({left:0, top:0});
b = new baseView();
b.render({left:500, top:0});
any help would be greatly appreciated, also if I'm doing anything really strangely I would love advice on how to do it better.
When you are setting the el property in a view, youre binding the view to an existing element in the dom, limiting yourself to create only one widget. What you actually want to do is let the view generate the element markup and just append all the generated widgets to a certain parent.
You can do that by setting the tagName, className and id attributes in the view.
For example:
var baseView = Backbone.View.extend({
tagName: 'div',
className: '.wrapper'
...
});
That will generate a div with a class of wrapper that you can append to a container.
Later on, you define a click event to create a new widget each time:
$('button').click(function() {
var newView = new baseView();
newView.render();
$('.container').append(newView.el); // now 'el' correspond to the div.wrapper you just created
});
It is considered a good practice among backbone developers to return this from the view's render method. That way you could mix the last two lines like this:
$('.container').append(newView.render().el);
Also, instead if instanciating the collection before the view's definition, people tend to pass the collection as a property of the constructor parameter:
var collection = new widgetCollection();
BV = new baseView({ collection: collection });
Now you can reference the collection inside the view by simply this.collection.
A bit of background
I'm trying to create a twitter like feed where the tweet/row will expand onclick, revealing more information.
The data is pulled from a JSON file (sent from the backend to the frontend).
I use backbone to render the data on the frontend.
Let's say my feed displays 10 rows, each row displays a few information then onclick the row/div expands to reveal more information.
The description field contains quite a lot of text therefore I'm applying a JavaScript ellipsis on it. I use Javascript ellipsis since the short description needs to be more than one line (don't think CSS ellipsis works for more than one line).
I created a plugin that will truncate the description text and onclick I want to remove the ellipsis and replace it by the full description (since the row will expand).
I created a plugin that will save the full description (before being truncated) into an array.
Issue
My idea was to compare the index of the row clicked (currentTarget) to the index of the rows saved (in the array) then replace the ellipsis text with the full description then expand the div with jQuery animate.
I'm not sure if there is a way to get an index from the backbone "click event" (in order to compare it to the index saved in the array)?
Feel free to let me know if there is a better way to approach this.
Thanks in advance
Here is my code:
Truncate & save original text functions
/**
* Plugins
*/
var arr = [];
$.fn.truncate = function(){
return this.each(function(index,element){
var elementText = $(element).text();
if(elementText.length > 165){
var truncated = elementText.trim().substring(0, 165).split(" ").slice(0, -1).join(" ") + "…";
}
$(element).text(truncated);
});
};
$.fn.getText = function(){
return this.each(function(index,element){
arr.push({
i: index,
v: $(element).text()
});
});
};
Backbone Model & Collections
/**
* Model
*/
var Task = Backbone.Model.extend();
/**
* Collections
*/
var RecentTasksList = Backbone.Collection.extend({
model: Task,
url: 'json/recentTasks.json'
});
Backbone Views
/**
* Views
*/
var RecentTasksView = Backbone.View.extend({
el: '.taskList',
template: _.template($('#recentTasksTemplate').html()),
render: function(){
_.each(this.model.models, function(data){
this.$el.append(this.template(data.toJSON()));
}, this);
$('.description').getText();
$('.description').truncate();
return this;
}
});
var FullTaskView = Backbone.View.extend({
el: '.taskContainer',
events: {
'click .task': 'showFullDetails'
},
showFullDetails: function(e){
var eTarget = $(e.currentTarget);
var $desc = $('.description');
if(eTarget.hasClass('expanded')){
eTarget.animate({
'height': '80px'
},
function(){
eTarget.removeClass('expanded');
});
}
else{
console.log($(eTarget).find($desc).html());
eTarget.animate({
//doesn't work lesser IE 8
'height': eTarget[0].scrollHeight
},
function(){
eTarget.addClass('expanded');
});
}
}
});
var AppView = Backbone.View.extend({
el: 'body',
initialize: function(){
//Recent Tasks
var recentTasksList = new RecentTasksList();
var recentTasksView = new RecentTasksView({
model: recentTasksList
});
recentTasksList.bind('reset', function(){
recentTasksView.render();
});
recentTasksList.fetch();
//Full Task Details
var fullTaskView = new FullTaskView();
}
});
var appView = new AppView();
Underscore template
<script id="recentTasksTemplate" type="text/template">
<div class="task clearfix">
<div class="image">
<img src="<%= image %>" />
</div>
<div class="details">
<h3 class="title"><%= title %></h3>
<div class="description">
<%= description %>
</div>
</div>
<div>
</script>
HTML
<div class="taskContainer">
<div class="taskList"></div>
</div>
EDIT
One last question. I added a tab to my page (similar call to action). Same type of information will be display onclick (I'm using the same template). For instance I now have RecentTask and PopularTask.
I created a view for the tabs containing click events. Do I need to instanciate the model & view & fetch the data each time or can I reuse the ones already initialized?
I created a new view for a second tab. Grabbing JSON file from the server:
var PopularTasksList = Backbone.Collection.extend({
model: Task,
url: 'json/popularTasks.json'
});
var PopularTasksView = Backbone.View.extend({
el: '.taskList',
render: function(){
$('.taskList').empty();
_.each(this.model.models, function(model){
var taskView = new TaskView({model: model});
this.$el.append(taskView.render().el);
}, this);
return this;
}
});
Then I created a tab view that will show the correct Tasks onclick.
var TabsView = Backbone.View.extend({
el: 'body',
events:{
'click .tabRecent': 'fetchDataRecentTasks',
'click .tabPopular': 'fetchDataPopularTasks'
},
fetchDataRecentTasks: function(){
var recentTasksList = new RecentTasksList();
var recentTasksView = new RecentTasksView({
model: recentTasksList
});
recentTasksList.bind('reset', function(){
recentTasksView.render();
});
recentTasksList.fetch();
},
fetchDataPopularTasks: function(){
var popularTasksList = new PopularTasksList();
var popularTasksView = new PopularTasksView({
model: popularTasksList
});
popularTasksList.bind('reset', function(){
popularTasksView.render();
});
popularTasksList.fetch();
}
});
I think you should create a new view for an individual task. Then in that view, you can handle the click, so you have access to the task model, and also access to the DOM of that view very easily.
Then you can get rid of your FullTaskView, and the jQuery plugins.
/**
* Model
*/
var Task = Backbone.Model.extend({
getShortDescription: function(){
var desc = this.get('description');
if(desc.length > 165){
return desc.trim().substring(0, 165).split(" ").slice(0, -1).join(" ") + "…";
}
return desc;
}
});
Add new TaskView, and change RecentTasksView to create/render them.
/**
* Views
*/
var TaskView = Backbone.View.extend({
template: _.template($('#recentTasksTemplate').html()),
events: {
'click': 'showFullDetails'
},
render: function(){
// pass the model json, plus the short description to the template
this.$el.html(this.template({
data: this.model.toJSON(),
shortDesc: this.model.getShortDescription()
}));
return this;
},
showFullDetails: function(){
// change text, show/hide, animate here
// In the view, this.$() will only match elements within this view.
// if expand...
this.$('.description').html(this.model.get('description'));
// if hide...
this.$('.description').html(this.model.getShortDescription());
}
});
var RecentTasksView = Backbone.View.extend({
el: '.taskList',
render: function(){
_.each(this.model.models, function(model){
// create a view for each task, render and append it
var taskView = new TaskView({model: model});
this.$el.append(taskView.render().el);
}, this);
return this;
}
});
Change template to use new data passed to it.
// access the model stuff with data.title, etc.
<script id="recentTasksTemplate" type="text/template">
<div class="task clearfix">
<div class="image">
<img src="<%= data.image %>" />
</div>
<div class="details">
<h3 class="title"><%= data.title %></h3>
<div class="description">
<%= shortDesc %>
</div>
</div>
<div>
</script>
EDIT:
A Backbone view is meant to manage a DOM element, so it is just a good idea to have each task be its own view instance. This makes it easier to do the expanding and changing the text based on the click. Also it is a best practice to not have code outside the view changing things inside its DOM element, so it is good to do that manipulation inside each task view.
This is similar to a TodoView in the Todo sample:
http://backbonejs.org/docs/todos.html
http://backbonejs.org/examples/todos/index.html
You can pass the template function any javascript object (even an object with functions, not just properties). Since you want to display some data that is not technically part of the model, passing that data object is just a way to get the stuff you need into the template.
I'm using a combination of handlebars and Backbone. I have one "container" view which has an array to hold child views. Whenever I add a new view, click events are not being bound.
My Post View:
Post.View = Backbone.View.extend({
CommentViews: {},
events: {
"click .likePost": "likePost",
"click .dislikePost": "dislikePost",
"click .addComment button": "addComment"
},
render: function() {
this.model.set("likeCount", this.model.get("likes").length);
this.model.set("dislikeCount", this.model.get("dislikes").length);
this.$('.like-count').html(this.model.get("likeCount") + " likes");
this.$('.dislike-count').html(this.model.get("dislikeCount") + " dislikes");
return this;
}, ...
My callback code in the "container" view which creates a new backbone view, attaches it to a handlebars template and shows it on the page:
success: _.bind(function(data,status,xhr) {
$(this.el).find("#appendedInputButton").val('');
var newPost = new Post.Model(data);
var newPostView = new Post.View({model: newPost, el: "#wall-post-" + newPost.id});
var source = $("#post-template").html();
var template = Handlebars.compile(source);
var html = template(newPost.toJSON());
this.$('#posts').append(html);
newPostView.render();
this.PostViews[newPost.id] = newPostView;
}, this), ...
Not sure what's going on, but this sort of code is run initially to set up the page (sans handlebars since the html is rendered server-side) and all events work fine. If I reload the page, I can like/dislike a post as well.
What am I missing?
I dont see you appending newPostView.render().el to dom .Or am i missing somehting?
Assuming the "#post-template" contains the "likePost" button. The newPostView is never added to the DOM.
Adding el to the new Post.View makes backbone search the DOM (and the element won't exist yet)
4 lines later a HTML string is added to the DOM (assuming the this.el is already in the DOM)
If you create the Post.View after the append(html) the element can be found and events would be fireing.
But the natural Backbone way would be to render the HTML string inside the Post.View render function, append the result to it's el and append that el to the #posts element.
success: function (data) {
var view = new Post.View({model: new Post.Model(data)});
this.$('#posts').append(view.render().el);
this.PostViews[data.id] = view;
}
js. I would like to know why my backbone.js is removing both items when I click on the X next to the album and artist.
i.e.
The Chronic-- Dr. Dre-- X
Ten-- Pearl Jam-- X
I appreciate any feedback that I could get that would allow me to only remove one item as opposed to both
Javacript:
(function($){
Backbone.sync = function(method, model, success, error){
success();
}
//Declare model
var Album = Backbone.Model.extend({
defaults: {
album: 'Logical Progresions vol. 1',
artist:'LTJ Bukem'
}
});
//Declare collection
var eList = Backbone.Collection.extend({
model: Album
});
//Declare the view for the Albums
var AlbumView = Backbone.View.extend({
el: $('div#main'),
template: _.template(
"<div class=\"alert\"> " +
" <span class=\"album\"><%= album %>--</span> " +
" <span claas=\"artist\"><%= artist %>--</span> " +
" <span class =\"delete\">X</span> " +
"</div>"
),
events: {
'click span.delete': 'deleteAlbum'
},
initialize: function(){
_.bindAll(this, 'render','unrender','deleteAlbum');
this.model.bind('remove', this.unrender);
},
// `unrender()`: Makes Model remove itself from the DOM.
unrender: function(){
this.$el.remove();
},
deleteAlbum: function(){
this.model.destroy();
},
render: function(){
$(this.el).append(this.template(this.model.toJSON()));
}
});
var appendItem = function(item){
var albumView = new AlbumView({
model: item
});
albumView.render();
}
//// set the stuff in motion
var elist = new eList();
elist.bind("add",function(listItem){appendItem(listItem)});
elist.add({
album: 'The Chronic',
artist: 'Dr. Dre'
});
elist.add({
album: 'Ten',
artist: 'Pearl Jam'
});
})(jQuery);
There are a few things I can point out.
First, your view - when you create multiple instances for each album - each share the same el. That is, div#main. Each time you add one, you're appending the template stuff to the el which is why you still see the other. But when you click on .delete and execute the this.$el.remove() you are removing everything in the el. Which includes the other view.
You should separate it out, each view should have it's own unique el.
el: 'div',
className: 'albumView'
When you add each album view, you can create the view and append it to your div#main
var view = new AlbumView();
$('#main').append(view.render().el); // the el refers to the subview (albumView) el.
This should keep each view happy with its own el and removal will only affect that view/model/DOMelement.
UPDATE - context of $('#main').append(view.render().el);
Basically, when you create the albumViews and append them the most ideal place to do this is in the larger context in which your div#main exists. For example, this might happen in your main js script in the header, or maybe even in a larger view that contains many albumView subviews. To illustrate the subview context:
var ParentView = Backbone.View.extend({
el: $('#main'),
render: function() {
this.addAllAlbums(); // On rendering the parent view, we add each album subview
return this;
},
addAllAlbums: function() {
var self = this;
// This loops through the collection and makes a view for each album model
this.collection.each(function(albumModel) {
self.addAlbumView(albumModel);
});
},
addAlbumView: function(albumModel) {
var view = new AlbumView({
'model': albumModel
});
// We are adding/appending each albumView to this view's el
this.$el.append(view.render().el);
// ABOVE: `this.$el` refers to ParentView el. view.render().el refers to the
// albumView or subview el.
// As explained before, now each album view has it's own el which exists in
// the parent view's this.el = `$('#main')`
}
});
// We create the parent BIG/ALLAlbumsView and toss into it the collection of albums
var BigAlbumsView = new ParentView({
'collection': albumsCollection
});
BigAlbumsView.render(); // Run the `render()` to generate all your album subviews
You might also want to store reference to those subviews by adding these lines in your code of the parent view. It will make cleaning up easier although it's not a big deal if you intend on cleaning up individual views through the subviews themselves.
// In your initialization, we create an array to store album subviews
this.albumViews = [];
// In `addAlbumView()` we push each view into the array so we have a reference
this.albumViews.push(view);
// When cleaning up, you just simply cycle through the subviews[] and remove/close
// each album subview
_.each(this.albumViews, function(albumView) {
albumView.$el.remove();
});
Hope this helps.
PS - Last note that I noticed. When you remove a view, I noticed you use remove() which is the way to get it out of the DOM. If you're making more complex subviews intertwined/tangled with event listeners to collections, models, and other views - you might want to read Derick Bailey's take on Zombie views and implementing a close() method that will both remove() and unbind() your view so there are no references to it and it can be garbage collected. Not the focus of this question but good for extra credit and possibly relevant since this has probably made your code more complicated. :-P
Removing Views - avoiding zombies