Dynamically set child View $el and have Backbone events fire - javascript

I have a parent view (and associated model), with several children Views with the same associated model. The parent view is defined statically from the HTML. These events are working fine.
The children views are created dynamically, and are ultimately different, but have some similar initial structure. The #id s will be different from each other (using the view id number) so that we can know which one is interacted with by the user. I have tried the following from reading around:
Adding the el declaration when I create the View (towards the end of the JS)
statically defining it, then trying to update it.
using _.ensureElement()
setting the el in the init()
But I just can't seem to get it for the children views on a fiddle.
Fiddle
JS: Parent
//The view for our measure
parentModule.View = Backbone.View.extend({
//
//This one is static, so I can set it directly, no problem, the events are working
//
el: $('#measure-container'),
events: {
'click .test': 'test'
},
test: function(){
alert('test');
},
initialize: function() {
this.template = _.template($('#instrument-template').html());
},
render: function(){
$(this.el).append(this.template(this.model.toJSON()));
return this;
}
});
return parentModule;
});
JS: Child
// THe child views are dynamic, so how do I set their id's dynamicall and still get the click events to fire?
//
childModule.View = Backbone.View.extend({
//
// Do I set the el here statically then override?
//
events: {
'click .remove-rep' : 'removeRepresentation',
'click .toggle-rep' : 'toggleRepType',
'click .sAlert': 'showAlert'
},
initialize: function(options) {
//
//Do I set the el here using ensure_element?
//
this.model=options.model;
},
render: function(){
//
// Do I Set it here when it renders?
//
this.template = _.template($('#rep-template').html());
$('#measure-rep-container').append(this.template());
return this;
},
showAlert: function() {
alert("This is an alert!");
}
});
JS: Instantiation
define( "app", ["jquery", "backbone", "parentModule", "childModule"], function($, Backbone, ParentModule, ChildModule) {
var app = {};
app.model = new ParentModule.Model({ name: "Snare", numOfBeats: 4 });
app.view = new ParentModule.View({ model: app.model });
app.view.render();
app.firstRepChildModel = new ChildModule.Model({ id: 1, type: 'circle', parentModel: app.model });
//
// Do I add the el as a parameter when creating the view?
//
app.firstRepChildView = new ChildModule.View({ el:'#rep'+app.firstRepChildModel.get('id'), model: app.firstRepChildModel });
app.firstRepChildView.render();
app.secondRepChildModel = new ChildModule.Model({ id: 2, type: 'line', parentModel: app.model });
//
// Do I add the el as a parameter when creating the view?
//
app.secondRepChildView = new ChildModule.View({ el:'#rep'+app.secondRepChildModel.id, model: app.secondRepChildModel });
app.secondRepChildView.render();
return app;
});
HTML:
<h3>Measure View</h3>
<div id="measure-container">
</div>
<!-- Templates -->
<script type="text/template" id="instrument-template">
<div class="instrument">
I am an instrument. My name is <%=name%>. <br/>
Here are my children repViews: <br/>
<div id="measure-rep-container">
<div class="btn btn-primary test">Add a rep</div>
</div>
</div>
</script>
<script type="text/template" id="rep-template">
<div class="rep" id="rep<%=this.model.id%>">
I am a repView <br/>
My ID is 'rep<%=this.model.id%>' <br/>
My el is '<%=this.$el.selector%>'<br/>
My type is '<%=this.model.type%>' <br/>
I have this many beats '<%=this.model.numOfBeats%>' <br/>
<div class="beatContainer"></div>
<div class="btn btn-danger remove-rep" id="">Remove this rep</div>
<div class="btn btn-primary toggle-rep" id="">Toggle rep type</div>
<div class="btn sAlert">Show Alert</div>
</div>
</script>

Every view has a associated el, whether you set it directly or not, if you don't set it then it's el is just a empty div.
In your code you aren't modifying your child view's el or attaching it to the DOM.
Try the following
render: function(){
this.template = _.template($('#rep-template').html());
//this sets the content of the el, however it still isn't attached to the DOM
this.$el.html(this.template());
$('#measure-rep-container').append(this.el);
return this;
},
Updated fiddle
As a separate point if you are going to be reusing the same template multiple times you might want to just compile it once, for example
childModule.View = Backbone.View.extend({
events: {
'click .remove-rep' : 'removeRepresentation',
'click .toggle-rep' : 'toggleRepType',
'click .sAlert': 'showAlert'
},
//get's called once regardless of how many child views you have
template: _.template($('#rep-template').html()),
render: function(){
this.$el.html(this.template());
$('#measure-rep-container').append(this.el);
return this;
},

Related

Updating a LayoutView's Model

JSBin link
Click the 'Clicks' link to see the console showing updated model data, but the original displayed HTML doesn't change.
Updating the model on the LayoutView doesn't seem to cause the view to update the displayed data. I thought that was part of the default interaction between Views and Models.
Backbone and Marionette do not bind model data to views automatically. You will have to listen that model to change within the view and update it.
For example, you can simply re-render the view completely when any data within the model changes:
initialize: function(){
this.listenTo( this.model, "change", this.render );
}
Or create a listener for a specific piece of data expected to change, and only update part of the view. This is preferred if the view is more complicated:
onRender: function(){
this.listenTo( this.model, "change:value", this.renderValue );
},
renderValue: function(){
/* This expects that you wrap the value with
<span class="value"></span>
in the template */
this.$('.value').text( this.model.get('value') );
}
Here's a complete working example:
var TestLayoutView = Mn.LayoutView.extend({
template: '#lvTemplate',
el: '#app',
events: { 'click .watchhere': 'updateValue'},
onRender: function(){
this.listenTo( this.model, "change:value", this.renderValue );
},
renderValue: function(){
this.$('.value').text( this.model.get('value') );
},
updateValue: function (clickedView) {
this.model.set('value', this.model.get('value') + 1);
}
});
var testLV = new TestLayoutView({
model: new Backbone.Model({ value: 15 })
});
testLV.render();
<script src='http://code.jquery.com/jquery.js'></script>
<script src='http://underscorejs.org/underscore.js'></script>
<script src='http://backbonejs.org/backbone.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.4.2/backbone.marionette.js'></script>
<script type="text/template" id="lvTemplate">
<a class='watchhere' href="#">Clicks <span class="value"><%= value %></span></a>
</script>
<div id="app"></div>

Backbone events not firing, no errors and element exists

Template:
<div class="view">
<input id="todo_complete" type="checkbox" <%= completed ? 'checked="checked"' : '' %> />
<label><%= title %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%= title %>" />
View:
var TodoView = Backbone.View.extend({
tagName: 'li',
todoTpl: _.template($('#item-template').html()),
events: {
'dblclick label': 'edit',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close'
},
initialize: function() {
console.log('Todo View initialized!');
this.$el = $('#todo');
console.log(this.$el);
this.render();
},
render: function() {
console.log('Todo View render started...');
console.log(this.model.attributes);
this.$el.html(this.todoTpl(this.model.attributes));
},
edit: function() {console.log('edit called!');},
close: function() {console.log('close called!');},
updateOnEnter: function(e) {
console.log(e);
}
});
There are no events when the page loads, and I'm able to see the rendered template. However if I double click on the label, keypress on the input or blur the other input, nothing happens. I'm expecting to see a log in the console. What am I doing wrong?
You lose your event bindings when you:
this.$el = $('#todo');
You're not supposed to directly assign to the $el or el properties, you're supposed to call setElement instead:
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view's delegated events from the old element to the new one.
Also, if you're going to be changing the view's el then there's no need for a tagName property. You could also specify the el when you create the view:
new TodoView({ el: '#todo' });
new TodoView({ el: $('#todo') });
If your #todo is actually the <ul> or <ol> then:
Leave the tagName alone.
Add the view's el to $('#todo') by calling append instead of calling setElement.
If this is the case then your code would look more like this:
var TodoView = Backbone.View.extend({
tagName: 'li',
//...
initialize: function() {
console.log('Todo View initialized!');
this.render();
},
//...
});
// And then where you create the view...
var v = new TodoView(...);
$('#todo').append(v.el);
So I was able to solve this problem by adding el to the properties passed into Backbone.View:
var TodoView = Backbone.View.extend({
tagName: 'li',
// ...
el: $('#todo')
// ..
});

Backbone.undo.js not tracking my second span and doesn't save start value

I have a little app where I want to edit spans by clicking them. I have two spans but when i edit second span, the first one automaticly change to the same value as the second one. I also cannot change to first value of spans, because first value by clicking undo is foo
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Backbone.Undo sample</title>
<style>
body {
font-family: sans-serif;
line-height: 140%;
}
#centered {
position: absolute;
top: 50%;
left: 50%;
margin-left: -180px;
margin-top: -120px;
}
</style>
</head>
<body>
<ol id="centered">
<li>
<span type="text" id="input" value="foo">span1 </span>
<button id="set">set</button>
</li>
<li>
<span type="text" id="input2" value="foo">span2 </span>
<button id="Button1">set</button>
</li>
<li>
<button id="undo">undo</button>
<button id="redo">redo</button>
</li>
</ol>
<script src="jquery-1.9.1.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="../Backbone.Undo.js"></script>
<script>
$(function () {
var model = new Backbone.Model({ "text": "foo" }),
View = Backbone.View.extend({
initialize: function () {
// If the model's text changes, update the view
this.model.on("change:text", function (model, text, options) {
if (text != this.$el.text()) {
this.$el.text(text);
}
}, this);
}
})
view = new View({
model: model,
el: $("#input")
})
// Now: The undo/redo part
var undoManager = new Backbone.UndoManager;
undoManager.register(model);
undoManager.startTracking();
$("#undo").on("click", function () {
undoManager.undo();
})
$("#redo").on("click", function () {
undoManager.redo();
})
$("#input").on("click", function () {
var element = document.getElementById("input");
element.innerHTML = prompt("#input", element.innerHTML);
model.set("text", $("#input").text());
})
$("#input2").on("click", function () {
var element = document.getElementById("input2");
element.innerHTML = prompt("#input2", element.innerHTML);
model.set("text", $("#input2").text());
})
})
</script>
</body>
</html>
From what I can see you are using 1 model and 1 view but you want them to act independently. So when you click span2 you are still setting the same model which is why they both change.
For this i would use seperate model and seperate views for the both spans like this
var model = new Backbone.Model({
"text": "span1"
});
var model2 = new Backbone.Model({
"text": "span2"
});
var View = Backbone.View.extend({
initialize: function () {
// If the model's text changes, update the view
this.model.on("change:text", function (model, text, options) {
if (text != this.$el.text()) {
this.$el.text(text);
}
}, this);
}
});
var view1 = new View({
model: model,
el: $("#input")
});
var view2 = new View({
model: model2,
el: $("#input2")
});
then register your other model in the undo register
// Now: The undo/redo part
var undoManager = new Backbone.UndoManager;
undoManager.register(model);
undoManager.register(model2);
undoManager.startTracking();
and finally change your click handler on span2 to edit the new model
$("#input2").on("click", function () {
var element = document.getElementById("input2");
element.innerHTML = prompt("#input2", element.innerHTML);
model2.set("text", $("#input2").text());
})
here is a fiddle with it all together
You can also be taking advantage of backbone more by having your view handle the events rather that doing this outside. here is an example
var View = Backbone.View.extend({
initialize: function () {
// If the model's text changes, update the view
this.model.on("change:text", function (model, text, options) {
if (text != this.$el.text()) {
this.$el.text(text);
}
}, this);
},
events: {
'click': 'editValue'
},
editValue: function () {
var data = prompt("#input2", this.model.get("text"));
if(data)
{
this.model.set("text", data);
}
}
});
now the view handles this change to the model and we also don;t have to get data from the html we keep it in the model at all times. here is an updated fiddle showing this technique http://fiddle.jshell.net/leighking2/2vx00s4b/9/
A More Backboney approach making use of a template for each model, a collection to hold your models and then a view for showing the collection and a view for showing each model. I have tried to add comments to show what the different parts are doing, ask if you want anything cleared up
Fiddle of this example
HTML
<script type="text/template" id="model-template">
<span type="text" class="input" value="foo"><%= text %></span>
<button class="remove">remove</button>
</script>
<div id="centered">
<ol id="collection-hook">
</ol>
<button id="undo">undo</button>
<button id="redo">redo</button>
<button id="add">add</button>
</div>
JS
var Model = Backbone.Model.extend({
defaults: {
text: "Click To Edit Text"
}
});
var Collection = Backbone.Collection.extend({
model: Model
});
var ModelView = Backbone.View.extend({
tagName: "li",
className: "model",
//get the template and cache it for future use
template:_.template($('#model-template').html()),
initialize: function() {
// If the model's text changes, update the view
this.listenTo(this.model, "change:text", this.render, this);
//if the model is removed from the collection close this view
this.listenTo(this.model, 'remove', this.close, this);
},
//events this view will be listening for
events: {
'click span.input': 'editValue',
'click button.remove': 'removeModel',
},
//get the models collection from it's self and tell the collection to remove the model
removeModel: function() {
this.model.collection.remove(this.model);
},
//ensure model and all events are destroyed and remove this view from the DOM
close: function() {
this.model.destroy();
this.remove();
this.unbind();
this.stopListening();
},
//render this view with the models data
render: function() {
//attach the generated template to this views $el
this.$el.html(this.template({
text: this.model.get("text")
}));
//return this view so collection view can decided where to place it on the DOM
return this;
},
editValue: function() {
var data = prompt("Change text", this.model.get("text"));
if (data) {
this.model.set("text", data);
}
}
});
var CollectionView = Backbone.View.extend({
el: $("#centered"),
//store a refrence to where you would like to add your models
collectinoHook: $("#collection-hook", this.$el),
initialize: function() {
this.undoManager = new Backbone.UndoManager;
this.undoManager.startTracking();
//when ever a new model is added to the collection call the function
//to render it
this.listenTo(this.collection, 'add', this.renderOne, this);
},
events: {
'click #add': 'addModel',
'click #redo': 'redo',
'click #undo': 'undo',
},
//render the collection items one at a time
renderOne: function(model) {
this.undoManager.register(model);
//create a new view using this model
var view = new ModelView({
model: model
});
//append the new view to you list
this.collectinoHook.append(view.render().el);
},
//use this to render the view with existing models in the collection
render: function() {
//clear the current views that have been rendered not the cleanest method as
//those views will still exist but this is a quick example
this.collectinoHook.html('');
this.collection.each(_.bind(function(model) {
this.renderOne(model);
},this));
},
undo: function() {
this.undoManager.undo();
},
redo: function() {
this.undoManager.redo();
},
addModel: function() {
//create a new model and add it to the collection
var model = new Model();
this.collection.add(model);
},
});
var collection = new Collection();
var collectionView = new CollectionView({
collection: collection
});

backbone and $el element

I'm trying to develop my first backbone application. All seems ok, but when i render the view and append some html to the $el, nothing is rendered in the page.
Rest service calls done ok, the Backbone.Router.extend is declared inside $(document).ready(function () {}); to ensure that the DOM is created.
Debugging my javascript, the el element get to contain the correct value in the innerHTML property, but when the whole page is rendered, this value doesn't appear in the page.
¿What am i doing wrong?
My View code:
window.ProductsListView = Backbone.View.extend({
id: 'tblProducts',
tagName: 'div',
initialize: function (options) {
this.model.on('reset', this.render, this);
},
render: function () {
// save a reference to the view object
var self = this;
// instantiate and render children
this.model.each(function (item) {
var itemView = new ProductListItemView({ model: item });
var elValue = itemView.render().el;
self.$el.append(elValue); // Here: the $el innerHTML is ok, but in the page it disappear. The id of element is div#tblProducts, so the element seems correct
});
return this;
}
});
window.ProductListItemView = Backbone.View.extend({
tagName: 'div',
template: _.template(
'<%= title %>'
),
initialize: function (options) {
this.model.on('change', this.render, this);
this.model.on('reset', this.render, this);
this.model.on('destroy', this.close, this);
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
// $(this.el).html('aaaaaa'); // This neither works: it's not a template problem
return this;
},
close: function () {
$(this.el).unbind();
$(this.el).remove();
}
});
Here i load products (inside Backbone.Router.extend). This is executed correctly:
this.productsList = new ProductsCollection();
this.productsListView = new ProductsListView({ model: this.productsList });
this.productsList.fetch();
And this is the html element i want to render:
<div id="tblProducts">
</div>
Thanks in advance,
From the code you have posted, you are not actually inserting your ProductsListView in to the DOM or attaching it to an existing DOM element.
The way I like to look at it is you have two types of Views:
Those that are dynamically generated based on data returned from the server
Those that already exist on the page
Usually in the case of lists, the list already exists on the page and it's items are dynamically added. I have taken your code and restructured it slightly in this jsfiddle. You will see that the ProductListView is binding to an existing ul, and ProductItemView's are dynamically appended when they are added to the Collection.
Updated jsfiddle to demonstrate Collection.reset
The el property exists within the view if it is rendered or not. You can't say it is ok there because Backbone will create an element if no element is passed (empty div).
If you want to render the view you should determine what is the container of the element? Do you have an html you want to attach the view to?
Try passing a container element by calling the view with an el like
this.productsListView = new ProductsListView({ model: this.productsList, el : $("#container") });
Of course you can create the view and attach it to the DOM later:
el: $("#someElementID") //grab an existing element
el.append(view.render().el);
Your view wont exist in the dom until you attach it somewhere.

Triggering event on an element in memory when testing Backbone Views

I am writing some integration tests for my Backbone views/models/collections. When I call render on my View, it simply renders a template to it's own el property, hence the html is simply stored in memory rather than on the page. Below is a simple model, and a view with a click event bound to a DOM element:
var model = Backbone.Model.extend({
urlRoot: '/api/model'
});
var view = Backbone.View.extend({
events: {
'click #remove': 'remove'
}
render: function () {
var html = _.template(this.template, this.model.toJSON());
this.$el.html(html);
},
remove: function () {
this.model.destroy();
}
});
I am using Jasmine to write my tests. In the test below all I want to do is spy on the remove function to see if it is called when the click event is fired for the element #remove which is present in the template I pass to the view.
// template
<script id="tmpl">
<input type="button" value="remove" id="remove"/>
</script>
// test
describe('view', function () {
var view;
beforeEach(function () {
view = new view({
template: $('#tmpl').html(),
model: new model()
});
});
it('should call remove when #remove click event fired', function () {
view.$('#remove').click();
var ajax = mostRecentAjaxRequest();
expect(ajax.url).toBe('/api/model');
expect(ajax.method).toBe('DELETE');
});
});
However, as the #remove element is in memory, and it hasn't actually been added to the DOM, I'm not sure how you would simulate the click event. In fact I'm not even sure if it's possible?
It may seem a bit strange to want to do this in a test, but with my tests I am trying to test behaviour rather than implementation, and this way I don't care what is happening in between - I just want to test that if the user clicks #remove a DELETE request is sent back to the server.
Looks to me like you forgot to call render() on the view before click()ing the button. And the model needs to have an id or backbone won't actually try to make a delete call to the server. I've tested plenty of views just like that before with no problems.
I just ran a similar test against jasmine 2.0 and jasmine-ajax 2.0.
live code:
var MyModel = Backbone.Model.extend({
urlRoot: '/api/model'
});
var MyView = Backbone.View.extend({
events: {
'click #remove': 'remove'
},
initialize: function(options) {
this.template = options.template;
},
render: function () {
var html = _.template(this.template, this.model.toJSON());
this.$el.html(html);
},
remove: function () {
this.model.destroy();
}
});
specs:
describe("testing", function() {
var view;
beforeEach(function() {
jasmine.Ajax.install();
view = new MyView({
template: '<input type="button" value="remove" id="remove"/>',
model: new MyModel({id: 123})
});
view.render();
});
it('should call remove when #remove click event fired', function () {
view.$('#remove').click();
var ajax = jasmine.Ajax.requests.mostRecent();
expect(ajax.url).toBe('/api/model/123');
expect(ajax.method).toBe('DELETE');
});
});

Categories