This question already has an answer here:
Backbone click event not firing in template View
(1 answer)
Closed 6 years ago.
I am getting the entire html code using jquery get() method and setting it on the el of backbone view.The view gets rendered perfectly but the click events i added are not firing. As i am a newbie to backbone i am not able to find the issue. Any help would be appreciated.
The currentTabID is contain the div id on which i want this html to be rendered.
view.js
var MyFirstView = Backbone.View.extend({
currentTabID:'',
initialize:function(){
this.render();
},
render: function (){
var self = this;
self.el = self.options.currentTabID;
$.get('resources/html/myBB.html', function(data) {
$(self.el).html(_.template(data));
});
return this;
},
events: {
'click .savebtnBB': 'invokeME'
},
invokeME: function (){
console.log('Fired');
}
});
Html looks something like below
myBB.html
<div id="sample_tab">
<div class="sub-main">
<form>
..
</form>
</div>
<div class="button">
<button class="savebtnBB">click me</button>
</div>
</div>
view.el is an actual dom element holding the event listeners for your view. You're replacing view's reference to that element with some number and appending the template to some other element.
Your view should act like an isolated unit as much as possible. Your code for appending it to something else should be outside the view, where you're creating it. Your code should look something like the following:
var MyFirstView = Backbone.View.extend({
initialize: function() {
var self = this;
$.get('resources/html/myBB.html', function(html) {
self.template = _.template(html);
this.render();
});
},
events: {
'click .savebtnBB': 'invokeME'
},
render: function() {
this.$el.html(this.template({ /*some data for template*/ }));
},
invokeME: function() {
console.log('Fired');
}
});
var viewInstance = new MyFirstView();
/*append to whatever you want*/
$(currentTabID).append(viewInstance.el);
Related
I'm trying to setup a little app in backbone where I can add items to a list and, when I click them, they'll be deleted. I've managed to add items to the list but when using model.destroy() nothing happens.
When I console.log the click event on the list models I get:
child {cid: "c0", attributes: Object, _changing: false, _previousAttributes: Object, changed: Object…}
for any item I click.
Code is below:
Html:
<h1>INDEX!</h1>
<form class="add-form">
<input type="text" name="name"/>
<hr />
<button type="submit" class="btn">Submit</button>
</form>
<h2>LIST STUFF</h2>
<ul class="blah">
{{#each indexCollection}}
<li class="li-class">{{name}}</li>
{{/each}}
</ul>
Javascript:
//Local Storage
App.Storage.Local = new Backbone.LocalStorage('localIndexList1-backbone');
//Index Model
App.Models.IndexModel = Backbone.Model.extend({
localStorage: App.Storage.Local,
defualts:{
name:''
},
urlRoot: '/'
});
//Index Collection
App.Collections.IndexCollection = Backbone.Collection.extend({
localStorage: App.Storage.Local,
model: App.Models.IndexModel,
initialize: function(){
console.log('Collection initialised');
},
url: '/'
});
//View for H1 and input form
App.Views.IndexView = Backbone.View.extend({
el: '.page',
events:{
'submit .add-form' : 'addNew',
'click' : 'deleteMe'
},
initialize: function(){
console.log('IndexView initialised');
},
addNew: function(ev){
// ev.preventDefault();
var submitEntry = $(ev.currentTarget).serializeObject();
var newEntry = new App.Models.IndexModel();
newEntry.save(submitEntry, {
success: function(newEntry){
// router.navigate('', {trigger: true});
console.log('SUCESSS!!!!!!!!!');
}
});
},
deleteMe: function(){
console.log(this.model);
//Whatever I put here will not work
}
});
//View for list
App.Views.ListView = Backbone.View.extend({
el: '.page',
initialize: function(){
console.log('ListView initialised');
},
template: Handlebars.compile($('#list').html()),
render: function(){
this.$el.html(this.template);
var that = this;
var indexCollection = new App.Collections.IndexCollection();
indexCollection.fetch({
success:function(indexCollection){
that.$el.html(that.template({indexCollection: indexCollection.toJSON()}));
}
});
}
});
Would anyone be able to help letting me know where I am going wrong?
Thanks!
Where are you creating one IndexView for each of your collection models? You should have an item view, configure its model to be one IndexModel, and move your delete code to that particular view. When you do that, you should also call remove in this item view.
This is why something like Backbone.Marionette helps a lot. Just throw in a CollectionView and you're done.
Think of it like this:
"list view" -> has a collection
"item view" -> has a single model
Anything you need to on the collection level (like adding a new one, re-loading, whatever), do it on your list view. Anything you need on model level (editing, saving, deleting), do it on your item view.
I have a Backbone application where I trigger a View through an event-click and the Views HTML gets rendered with HandlebarsJS and displays data from a collection. So far it works except that my HTML gets repeated for each entry
My HTML looks like this:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul>
<li>
{{brand}}
<p>{{country}}</p>
</li>
</ul>
</div>
Right now, it duplicates the whole HTML code-block, including <header> and <h1>-tag for each entry but what I want to achieve is that my HTML would look like this:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul>
<li>
Audi
<p>Germany</p>
</li>
<li>
Hyundai
<p>South Korea</p>
</li>
<li>
Fiat
<p>Italy</p>
</li>
</ul>
</div>
My backbone View looks like this:
define(['backbone','handlebars', 'text!templates/Cars.html'],
function(Backbone,Handlebars, Template) {
'use strict';
var CarsView = Backbone.View.extend({
template: Handlebars.compile(Template),
events: {
},
initialize: function () {
_.bindAll(this, 'render');
},
render: function() {
var self = this;
self.collection.each(function(model){
self.$el.append(self.template({
brand:model.get('brand'),
country:model.get('country')
})
);
});
return this;
}
});
return CarsView;
}
);
Of course i could define the most of the HTML on the index page, wrap it in a DIV and do display:none and only write the <li>-tag in the Handlebars HTML template, but I want to avoid that and since the data what is returned are strings, I cant do the {{#each}}-thing... so, is there any solution for this?
To add onto what guzmonne said, the reason you are seeing this is because you are looping over the entire template. What you should be doing is taking the <li>'s and making a new template specifically for them. I've modified your existing code to show how something like what you are trying to do can be accomplished.
The CarsView Handlebars template:
<header>
<span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
<ul id="cars_list" />
</div>
The CarsView Backbone View:
var CarsView = Backbone.View.extend({
template: Handlebars.compile(Template),
events: {},
initialize: function () {
_.bindAll(this, 'render');
},
render: function() {
this.$el.html(this.template()); // this now only renders once
this.addAll();
return this;
},
addOne: function(car) {
var view = new CarView({model: car});
this.$("#cars_list").append(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne, this);
}
});
The main differences between what I have done and your original CarsView is that firstly, the template itself doesn't contain any <li>'s. Instead, there is a placeholder ID which I have titled "cars_list". This container will give us an entry point to loop through the cars collection and add each item. Secondly, we aren't looping through the collection and re-rendering the CarsView. Instead, we take what CarsView dropped into the DOM and manually apppend to it from there.
Normally when dealing with collections, you can leverage Backbone's this.listenTo() function, which can take an event such as "reset" or "add" and tie it to a function. Since it would appear that the collection has already been fetched, we simply do this.addAll() after the CarsView template has been rendered. It is in here that the collection is looped and added.
To accomplish this, you will need another Backbone view, and another template...
The CarView Handlebars template:
{{brand}}
<p>{{country}}</p>
The CarView Backbone View:
var CarView = Backbone.View.extend({
tagName: "li",
template: Handlebars.compile(Template),
events: {},
initialize: function () {
// model event listeners (in case these list items can be edited/removed)
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
The CarView view is short and simple, but takes care of a lot for you. As you can see, this view will generate an <li> tag and take the contents of the model and send it to the handlebars template. No more having to deal with manually fetching model attributes, which is an added bonus.
I am a backbone newbie and I am trying to develop a Todo like App.
I have a Main view which is a list view and it has subviews. - the subview content can be edited on double click and it would get saved when the enter key is pressed. - very similar to todo example given in backbone github code.
var SubView = Backbone.View.extend({
tagName: "li",
events: {
"dblclick" : "show_edit_view",
"blur .element" : "close_edit_view",
"keypress .element" : "save_edit_view",
"click button.remove" : "remove_question"
},
initialize: function(){
this.render();
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html(_.template($("#sub_view_template").html(),this.model.toJSON()));
return this;
},
show_edit_view: function() {
this.$el.find("div.view").addClass("no_show");
this.$el.find("input").removeClass("no_show");
},
close_edit_view: function(){
this.$el.find("div.view").removeClass("no_show");
this.$el.find("input").addClass("no_show");
},
save_edit_view: function(e){
if (e.keyCode == 13) {
this.model.save({name: e.currentTarget.value});
this.close_edit_view();
}
}
});
And the template for this is
<script id="sub_view_template" type="text/x-template">
<div class="view"><%= name %></div>
<input class="element no_show" value="<%= name %>" type="text" /> <button class="remove">Remove</button>
</script>
This one works fine, the model is updated in the view and the update post request is sent to the server.
But, when I change the initialization and save_edit_view functions, only the first change event is fired and not the change events.
initialize: function(){
this.render();
this.listenTo(this.model, "change", this.render);
this.input = this.$("input.element");
},
save_edit_view: function(e){
if (e.keyCode == 13) {
this.model.save({name: $(this.input).val()});
this.close_edit_view();
}
}
I was wondering what could the problem be?
Thanks for any help!!!
The problem is you are referring to only one object. This means when you make the assignment:
this.input = this.$('input.element'); // match the current elements.
You are only getting the value from that exact object. After the first change, this.input is not the same object that contains your new value, and fails to save the model with a new value.
A demonstration that may help:
console.log(this.$('input.element') != this.$('input.element')); // true
This is why the following would work:
save_edit_view: function(e){
if (e.keyCode == 13) {
this.model.save({name: this.$('input.element').val()});
this.close_edit_view();
}
}
I guess this.$("input.element"); refers to the first item from the list.
And when you first time change model value with the value from the first item it works. But second time it doesn't works because the value of the first item still the same.
That is why you have to get input value from the event - e.currentTarget.value
this is below is code which i am using for simple task add , view is not getting render, i am not able to find the mistake ,
<!doctype html>
<html lang="en">
<head>
<title>Calculator</title>
<link rel="stylesheet" href="styles/bootstrap/css/bootstrap.min.css">
<script src="js/libs/jquery-1.9.1.min.js"></script>
<script src="js/libs/underscore-min.js"></script>
<script src="js/libs/backbone-min.js"></script>
<script type="text/template" id="display-template">
<div class="row">
<div class="span4">
<%=content%>
</div>
</div>
</script>
<script language="javascript">
var cdate;
var tasks={};
var app = app || {};
// App view responsible for rendering app
app.TaskView = Backbone.View.extend({
el: $('#tasks'),
template: _.template($('#display-template').html()),
initialize: function () {
this.render();
},
render: function () {
console.log("render called");
console.log(this.template());
this.$el.html(this.template());
}
});
app.task = Backbone.Model.extend({
defaults:{
content:null
}
});
app.bUsers = Backbone.Collection.extend({
model : app.task,
initialize: function(models, args) {
this.bind('add', this.renderone);
this.bind('remove', this.destroy); },
renderone:function(user){
console.log(user);
var view = new app.TaskView({model: user});
},
destroy:function(user){
$(user.view.el).remove();
}
});
app.Users = new app.bUsers();
$(document).ready(function() {
cdate=new Date();
$("#cdate").html(new Date());
$("#pre").click(function(){
cdate=new Date(cdate.getTime()-(1*24*3600*1000));
$("#cdate").html(cdate);
});
$("#next").click(function(){
cdate=new Date(cdate.getTime()+(1*24*3600*1000));
$("#cdate").html(cdate);
});
$("#submit").click(function(){
if(tasks[cdate]==undefined) tasks[cdate]=[];
tasks[cdate].push($("#task").val());
// app.appView = new app.TaskView({
// model: new app.task({"content":$("#task").val()})
// });
var data ={"content":$("#task").val()};
app.Users.add(data);
});
});
</script>
</head>
<body>
<a id="pre" href="#">Prev</a>
<div id="cdate"></div>
<a id="next" href="#">Next</a>
<input type="text" id="task" ></input>
<input type="button" value="submit" id="submit" ></input>
<div id="tasks"></div>
</body>
Oye, you've got a few problems.
To answer your specific question, your render method of your view should take your view's model instance and get something from this.model.toJSON() it to get at its data to pass to the template method (toJSON really returns "JSONable" objects).
But that's not all.
Besides a few html issues, you also have stylistic problems.
Collections generally should not be concerned with views, only data (*). Views should be concerned with with collections and models. Collections communicate to views via event binding, which I see you are doing. However, for reuse purposes, you may have more than one combination views that might want to listen to events in the collection. By setting up the event binding in the collection, you've effectively limited your collection for only one use.
Views can do alot. There's not much reason to manually add DOM event handlers when you can code the view to do it for you.
I haven't written Backbone in a little while (not by choice!), but generally found it was a good idea to have a view dedicated to the collection, and then have a separate model view that the collection view might create or destroy based upon whatever events took place.
Here's a bit of a cleanup of your code to give you a starting example:
http://jsfiddle.net/jfcox/SmPNv/
HTML:
<a id="pre" href="#">Prev</a>
<div id="cdate"> </div>
<a id="next" href="#">Next</a>
<input type="text" id="task" />
<input type="button" value="add" id="submit" />
<div id="tasks"></div>
Backbone definitions:
var defs = defs || {};
//first define the data handlers
defs.Task = Backbone.Model.extend({
defaults: function () {
return {
content: null,
addDate: (new Date()).toString()
};
}
});
defs.Users = Backbone.Collection.extend({
model: defs.Task
});
// App view responsible for rendering app
defs.SingleTaskView = Backbone.View.extend({
//since we can't control where the js is loaded, go ahead and make the template inline for jsFiddle demo.
tagName: 'div',
template: _.template('<div class="row"> <div class="span4"><%=content%></div> <em><%=addDate%></em> <button class="remove"> remove</remove> </div>'),
events: {
"click button.remove": "remove"
},
initialize: function (opts) {
this.model.on('change', this.render, this);
},
render: function () {
console.log("render called");
var modelBare = this.model.toJSON();
return this.$el.html(this.template(modelBare));
},
remove: function () {
//removes from local collection, does not delete on server
//for that, you'd want `this.model.destroy`
this.collection.remove(this.model);
//removes this view's element.
this.$el.remove();
}
})
defs.TasksView = Backbone.View.extend({
el: 'body',
events: {
"click #pre": "doPrevious",
"click #next ": "doNext",
"click #submit ": "doSubmit"
},
cdate: null,
initialize: function (opts) {
this.cdate = new Date();
this.render();
this.collection.on('add', this.renderone, this);
},
render: function () {
$("#cdate").html(this.cdate.toString());
},
doPrevious: function () {
this.cdate = new Date(this.cdate.getTime() - (1 * 24 * 3600 * 1000));
$("#cdate").html(this.cdate.toString());
},
doNext: function () {
this.cdate = new Date(this.cdate.getTime() + (1 * 24 * 3600 * 1000));
$("#cdate").html(this.cdate.toString());
},
doSubmit: function () {
var data = {
"content": $("#task").val()
};
this.collection.add([data]);
},
renderone: function (userModel) {
var view = new defs.SingleTaskView({
model: userModel,
collection: this.collection
});
this.$el.find('#tasks').append(view.render());
}
});
The application, itself.
var app = app || {};
app.users = new defs.Users();
(function ($) {
$(document).ready(function () {
app.usersview = new defs.TasksView({
collection: app.users
});
});
})(jQuery);
(*) This is a guideline, not an absolute rule, of course. If you think a collection might work as some sort of workflow manager, etc, that might be fine, but that's an advanced topic.
Edit: I included the template inline, partially for reasons that I don't trust jsFiddle with inline "text" scripts. I'm not recommending any way to handle that, just that's how I did it here.
This is my code:
$(function (){
var Slide = Backbone.Model.extend({
defaults: {
castid :1,
id :1
},
urlRoot: function(){
return 'slidecasts/' + this.get("castid") + '/slides/';
},
});
var SlideView = Backbone.View.extend({
el: $("#presentation"),
events: {
'click #next': 'next',
'click #previous': 'previous',
},
initialize: function(){
_.bindAll(this, 'render', 'next');
this.model.bind('change', this.render);
this.render();
},
render: function(){
this.model.fetch();
var variables = {
presentation_name: "This is a Slide-Number: ",
slidenumber: "xxx",
imageurl: this.model.url() +"/"+ this.model.get('imageLinks'),
slide_content: this.model.get("content")};
var template = _.template( $("#slide_template").html(), variables );
this.$el.html( template );
return this;
},
next: function(){
console.log(this.model.id);
this.model.nextslide();
},
previous: function(){
console.log("previous function in view");
}
});
testslide = new Slide();
var slideView = new SlideView({model: testslide});
});
This works fine but in the debug console I always see a GET Request to "slidecasts/1/slides/1/undefined" which of course fails. I don't really understand where I trigger this get request.
Edit - the template code
<script type="text/template" id="slide_template">
<label>Presentation <%= presentation_name %> </label> <br/>
<img src="<%= imageurl %>" id="slide_pic" /> <br/>
<textarea id="slide_content">
<%= slide_content %>
</textarea>
<div id="next">next slide </div>
<div id="previous">previous slide </div>
</script>
You have an asynchronous problem.
This is the sequence of events:
You call this.model.fetch() to populate the model.
You say variables.imageurl = this.model.url() + '/' + this.model.get('imageLinks').
The (asynchronous) fetch hasn't returned yet so this.model.get('imageLinks') is undefined.
You build the HTML and use this.$el.html(template) to update the page.
The browser renders your HTML using the incorrect imageurl from 2.
A bad GET request is logged because of 5.
The fetch from 1 returns from the server and triggers a 'change' event.
The 'change' event triggers a new call to render.
This render call has a fully populated this.model so variables.imageurl is correct and the HTML comes out right this time.
If you let the fetch trigger the render then the problem will go away:
initialize: function(){
_.bindAll(this, 'render', 'next');
this.model.bind('change', this.render);
this.model.fetch();
},
render: function() {
// As before except no this.model.fetch()
}
How I can't see the template you are using I'm just guessing here:
The problem is in this line:
this.model.url() +"/"+ this.model.get('imageLinks'),
Your template is trying to define an <img> element with such URL but the imageLinks attribute is undefined.