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.
Related
Here is, what I have achieve so far. I am having a div inside my first template. When I am showing another layoutView inside that div. it is showing the following error.
Uncaught Error: An "el" #nestedDiv must exist in DOM
HTML -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MarionetteJS</title>
</head>
<body>
<div id="container"><div>
<script type='text/template' id='myTemplate'>
<h2><%=heading%></h2>
<div id='nestedDiv'></div>
</script>
<script type='text/template' id='innerTemplate'>
<h2><%=nestedHeading%></h2>
</script>
<script src='_assets/js/_lib/jquery-1.7.2.min.js'></script>
<script src='_assets/js/_lib/underscore.js'></script>
<script src='_assets/js/_lib/backbone.js'></script>
<script src='_assets/js/_lib/backbone.marionette.js'></script>
<script src='_assets/js/layoutView.js'></script>
</body>
</html>
JS -
//Application Object
var myApp = new Marionette.Application({
regions: {
main: '#container'
}
});
//First Model
var TaskModel = Backbone.Model.extend({
defaults: {
'heading' : 'Welcome to Backbone'
}
});
//Second Model
var Person = Backbone.Model.extend({
defaults: {
'nestedHeading' : 'This is a subheading.'
}
});
//View for Div #nestedDiv
var PersonView = Marionette.ItemView.extend({
template: '#innerTemplate'
});
//View for main Region
var TaskView = Marionette.LayoutView.extend({
template : '#myTemplate',
onShow: function() {
var person = new Person();
var personView = new PersonView({model: person});
var PersonLayoutView = Marionette.LayoutView.extend({
regions: {
'foo' : '#nestedDiv'
}
});
var obj = new PersonLayoutView();
obj.foo.show(personView);
}
});
var taskModel = new TaskModel();
var taskView = new TaskView({model:taskModel});
myApp.main.show(taskView);
and here is JSBin Link - http://jsbin.com/dusica/1/edit?html,js,console,output
onShow is a callback on the Marionette Region object and cannot be called on the layout directly.
You probably want to:
1 Call render() on the instantiated LayoutView, in order to render the template in the DOM;
2 Instantiate a new view to show within a Region that was defined on the LayoutView;
3 Show the view within the region. If you need it, you can use obj.foo.onShow() as a callback after the view was rendered within the region;
According to the docs:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.layoutview.md
interactions with Marionette.Region will provide features such as
onShow callbacks, etc. Please see the Region documentation for more
information.
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md
"show" / onShow - Called on the view instance when the view has been
rendered and displayed.
"show" / onShow - Called on the region
instance when the view has been rendered and displayed.
Sidenote: in case you would be tempted to use onRender() (LayoutView extends from ItemView), don't. onRender does not mean that the view is present in the DOM, but rather that it is prepared for insertion.
That error basically means that your #nestedDiv is not yet in DOM when you are trying to show the view. And that is understandable as you have not showed obj (a PersonLayoutView) in any region.
In fact, you do not need another nested layout view. Consider modifying your view for main region to:
//View for main Region
var TaskView = Marionette.LayoutView.extend({
template : '#myTemplate',
regions: {
'personLayoutRegion': '#nestedDiv'
},
onShow: function() {
var person = new Person();
var personView = new PersonView({model: person});
this.personLayoutRegion.show(personView);
}
});
It works at: http://jsbin.com/pecoxujose/2/
I currently have a composite view, and I would like for each ItemView to render based on its index.
For example, I want to add a class to an every third ItemView.
The solution I'm leaning towards is altering appendHtml() to add a class to the view every third time. I've put the code for this below.
Is there any advantage to using getItemView()? A disadvantage I see is that it doesn't have direct access to the index.
Templates
<script id="list-item" type="text/html">
<%= name %>
</script>
<script id="list-layout" type="text/html">
<div class='collection'>
<h3><%= name %></h3>
<ul></ul>
</div>
</script>
JS
var ListItemView = Backbone.Marionette.ItemView.extend({
template: '#list-item',
tagName: 'li'
});
var ListComposite = Backbone.Marionette.CompositeView.extend({
itemView: ListItemView,
itemViewContainer: "ul",
template: '#list-layout',
appendHtml: function(cv, iv, index){
if ((index + 1) % 3 == 0) iv.$el.addClass('rowend');
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
}
});
One option would be to use the buildItemView
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md#collectionviews-builditemview
Now you won't have direct access to the index, but you can use underscores methods to fix that (these should all be augmented onto backbone collections, in particular this one http://underscorejs.org/#indexOf).
The main benefit you get is that you can directly influence the classes of the itemviews
var ListComposite = Backbone.Marionette.CompositeView.extend({
itemView: ListItemView,
itemViewContainer: "ul",
template: '#list-layout',
buildItemView: function(item, ItemViewType, itemViewOptions){
var index = this.collection.indexOf(item);
var options = _.extend({model: item}, itemViewOptions, {className:"someClassName" + index});
var view = new ItemViewType(options);
return view;
},
});
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.
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