Updating a LayoutView's Model - javascript

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>

Related

Dynamically set child View $el and have Backbone events fire

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;
},

Inputs clearing issue

I have a backbone.js app where I have three views (two of them are subviews)
I put a simplified version of the app here:
http://jsfiddle.net/GS58G/
The problem is if I create a Product (which defaults to a Book subview), and enter in "A Good Book", then click "Add Product", the "A Good Book" is cleared. How do I fix this so it saves the "A Good Book" when you add another product?
My subviews for the product book, magazine, and video look like:
var ProductBookView = Backbone.View.extend({
render: function () {
this.$el.html( $("#product_book_template").html() );
}
});
var ProductVideoView = Backbone.View.extend({
render: function () {
this.$el.html( $("#product_video_template").html() );
}
});
var ProductMagazineView = Backbone.View.extend({
render: function () {
this.$el.html( $("#product_magazine_template").html() );
}
});
I've forked your jsfiddle and updated it. Here is the working jsfiddle.
Issue is in this statement:
var productBookView = new ProductBookView({
el: $('.product-view')
});
You need to update it with:
var productBookView = new ProductBookView({
el: $('.product-view:last')
});
Reason is, the el element of ProductBookView should be last .product-view. In the code provided by you el is assigned all div elements existing in the DOM having class by name product-view. Hence every time you add a new product, all div elements with class name product-view is updated with product_book_template html. Hence the input box gets cleared off.
you have to use template ..
...
...
my_template: _.template($("#store_template").html()),
initialize: function() {
this.render();
},
events: {
"click #addProduct": "addProduct"
},
render: function() {
this.$el.html( this.my_template() );
},
....
..

Backbone collection is not updating when api is polled

I'm having some trouble getting change events to fire when a model is updated via polling of an endpoint. I'm pretty sure this is because the collection is not actually updated. I'm using the new option (update: true) in Backbone 0.9.9 that tries to intelligently update a collection rather than resetting it completely.
When I insert a console.log(this) at the end of the updateClientCollection function, it appears that this.clientCollection is not updating when updateClientCollection is called via setInterval. However, I do see that the endpoint is being polled and the endpoint is returning new and different values for clients.
managementApp.ClientListView = Backbone.View.extend({
className: 'management-client-list',
template: _.template( $('#client-list-template').text() ),
initialize: function() {
_.bindAll( this );
this.jobId = this.options.jobId
//the view owns the client collection because there are many client lists on a page
this.clientCollection = new GH.Models.ClientStatusCollection();
this.clientCollection.on( 'reset', this.addAllClients );
//using the initial reset event to trigger this view's rendering
this.clientCollection.fetch({
data: {'job': this.jobId}
});
//start polling client status endpoint every 60s
this.intervalId = setInterval( this.updateClientCollection.bind(this), 60000 );
},
updateClientCollection: function() {
//don't want to fire a reset as we don't need new view, just to rerender
//with updated info
this.clientCollection.fetch({
data: {'job': this.jobId},
update: true,
reset: false
});
},
render: function() {
this.$el.html( this.template() );
return this;
},
addOneClient: function( client ) {
var view = new managementApp.ClientView({model: client});
this.$el.find( 'ul.client-list' ).append( view.render().el );
},
addAllClients: function() {
if (this.clientCollection.length === 0) {
this.$el.find( 'ul.client-list' ).append( 'No clients registered' );
return;
}
this.$el.find( 'ul.client-list' ).empty();
this.clientCollection.each( this.addOneClient, this );
}
});
managementApp.ClientView = Backbone.View.extend({
tagName: 'li',
className: 'management-client-item',
template: _.template( $('#client-item-template').text() ),
initialize: function() {
_.bindAll( this );
this.model.on( 'change', this.render );
},
render: function() {
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
From what I can gather from your code, you're only binding on the reset event of the collection.
According to the docs, Backbone.Collection uses the .update() method after fetching when you pass { update: true } as part of your fetch options.
Backbone.Collection.update() fires relevant add, change and remove events for each model. You'll need to bind to these as well and perform the relevant functions to update your UI.
In your case, you could bind to your existing addOneClient method to the add event on your collection.
In your ClientView class, you can bind to the change and remove events to re-render and remove the view respectively. Remember to use listenTo() so the ClientView object can easily clean-up the events when it remove()'s itself.

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');
});
});

Backbone basic app, is this how it should be done?

Hope you can have a quick look at what I'm doing here. Essentially, am I doing it right?
Live demo of it here too: http://littlejim.co.uk/code/backbone/messing-around/
I just wanted to get a solid understanding in Backbone before I go too wild. So this is a simple demonstration of creating a collection from a JSON object, passing it to a view and handling simple events. But am I approaching this right? What can I do that's better?
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Showing a simple view with events</title>
<script type="text/javascript" src="../../media/scripts/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="../../media/scripts/underscore-min.js"></script>
<script type="text/javascript" src="../../media/scripts/backbone-min.js"></script>
<script type="text/javascript" src="application.js"></script>
</head>
<body>
<header>
<h1>Showing views from a collection and basic events</h1>
<p>The list below is made from JSON, passed to the view as a collection and has basic events</p>
</header>
<article>
</article>
</body>
</html>
Here is the JavaScript I currently have. I just need to know if I'm approaching this correctly?
window.App = {
// namespaces
Controller: {},
Model : {},
Collection : {},
View : {},
// code that starts when the app is first fired
initialize : function () {
var collection = new App.Collection.Inputs([
{title: "Item 1"},
{title: "Item 2"},
{title: "Item 3"}
]);
var view = new App.View.InputSet({collection: collection});
$('article').html(view.render().el);
}
}
/*
Collection: Inputs */
App.Collection.Inputs = Backbone.Collection.extend();
/*
View: _Input */
App.View._Input = Backbone.View.extend({
events: {
"click a": "close"
},
// called as soon as a view instance is made
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render", "close");
},
// backbone required method, which renders the UI
render: function() {
// this is using underscore templating, which can be passed context
$(this.el).html(_.template('<p><%=title%> [close]</p>', this.model.toJSON()));
return this;
},
close: function() {
// removes the UI element from the page
$(this.el).fadeOut(300);
return false; // don't want click to actually happen
}
});
/*
View: InputSet, uses _Input */
App.View.InputSet = Backbone.View.extend({
events: {
'click a': 'clear'
},
initialize: function() {
// this makes the render, clear etc available at this
// if not setting this, both render() and clear() method will not have themselves in this
_.bindAll(this, "render");
},
// backbone required method, which renders the UI
render: function() {
var that = this;
views = this.collection.map(function(model) {
var view = new App.View._Input({model: model});
$(that.el).append(view.render().el);
return view;
});
$(that.el).append('[clear]');
return this;
},
clear: function() {
$(this.el).find('p').fadeOut(300);
}
});
// wait for the dom to load
$(document).ready(function() {
// this isn't backbone. this is running our earlier defined initialize in App
App.initialize();
});
This looks fine to me. However, I found that things can get tricky once you start doing non-trivial stuff: complex views, nested collections etc.
One thing that could be done differently is that instead of generating input views using collection.map you could bind the collection's add event to a function that generates an _Input view for that item in the collection instead. So you'd have something like this in your InputSet view:
initialize: function() {
_.bindAll(this, "addInput", "removeInput");
this.collection.bind("add", this.addInput);
this.collection.bind("remove", this.removeInput);
}
addInput: function(model) {
var view = new App.View._Input({model: model});
$(this.el).append(view.render().el);
}
I looks good to me - really the only thing I would suggest is that you bind the collection's 'change' event to _Input.render that way changes to your collection automatically re-render the view:
// called as soon as a view instance is made
initialize: function() {
_.bindAll(this, "render", "close");
this.collection.bind('change', this.render);
},
Other than that I think it looks good!

Categories