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.