My code is below, when I run it, it shows error - <HTMLAudioElement> has no method 'set'.
Why it''s happening? I've bound my functions to model, but it seems to work wrongly:
var Player = Backbone.Model.extend({
initialize: function(){
_.bind(this.ontimeupdate, this);
_.bind(this.onprogress, this);
},
setAudio: function(ogglink, mp3link, ontimeupdate){
var el = document.createElement("audio");
el.addEventListener('timeupdate', this.ontimeupdate);
this.audiotag = el;
},
ontimeupdate: function() {
this.set("curtime", this.currentTime);
}
});
Unlike bindAll (which you should be using as you make several binds), bind only returns the binded function (equivalent to ECMA's bind method). So...
this.ontimeupdate = _.bind(this.ontimeupdate, this);
this.onprogress = _.bind(this.onprogress, this);
Or
_.bindAll(this);
Or
_.bindAll(this, 'ontimeupdate', 'onprogress');
If you're binding those methods to this (your Player instance) then your ontimeupdate method should probably look more like this:
ontimeupdate: function() {
this.set("curtime", this.audiotag.currentTime);
}
Related
Trying to make a reasonable teaching model of Backbone that shows proper ways to take advantage of backbone's features, with a grandparent, parent, and child views, models and collections...
I am trying to change a boolean attribute on a model, that can be instantiated across multiple parent views. How do I adjust the listers to accomplish this?
The current problem is that when you click on any non-last child view, it moves that child to the end AND re-instantiates it.
Plnkr
Click 'Add a representation'
Click 'Add a beat' (you can click this more than once)
Clicking any beat view other than the last one instantiates more views of the same beat
Child :
// our beat, which contains everything Backbone relating to the 'beat'
define("beat", ["jquery", "underscore", "backbone"], function($, _, Backbone) {
var beat = {};
//The model for our beat
beat.Model = Backbone.Model.extend({
defaults: {
selected: true
},
initialize: function(boolean){
if(boolean) {
this.selected = boolean;
}
}
});
//The collection of beats for our measure
beat.Collection = Backbone.Collection.extend({
model: beat.Model,
initialize: function(){
this.add([{selected: true}])
}
});
//A view for our representation
beat.View = Backbone.View.extend({
events: {
'click .beat' : 'toggleBeatModel'
},
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
},
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
},
toggleBeatModel: function() {
this.model.set('selected', !this.model.get('selected'));
this.trigger('beat:toggle');
}
});
return beat;
});
Parent :
// our representation, which contains everything Backbone relating to the 'representation'
define("representation", ["jquery", "underscore", "backbone", "beat"], function($, _, Backbone, Beat) {
var representation = {};
//The model for our representation
representation.Model = Backbone.Model.extend({
initialize: function(options) {
this.idAttr = options.idAttr;
this.type = options.type;
this.beatsCollection = options.beatsCollection;
//Not sure why we have to directly access the numOfBeats by .attributes, but w/e
}
});
//The collection for our representations
representation.Collection = Backbone.Collection.extend({
model: representation.Model,
initialize: function(){
}
});
//A view for our representation
representation.View = Backbone.View.extend({
events: {
'click .remove-representation' : 'removeRepresentation',
'click .toggle-representation' : 'toggleRepType',
'click .add-beat' : 'addBeat',
'click .remove-beat' : 'removeBeat'
},
initialize: function(options) {
if(options.model){this.model=options.model;}
// Dont use change per http://stackoverflow.com/questions/24811524/listen-to-a-collection-add-change-as-a-model-attribute-of-a-view#24811700
this.listenTo(this.model.beatsCollection, 'add remove reset', this.render);
this.listenTo(this.model, 'change', this.render);
},
render: function(){
// this.$el is a shortcut provided by Backbone to get the jQuery selector HTML object of this.el
// so this.$el === $(this.el)
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#representation-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#measure-rep-container').append(this.el);
_.each(this.model.beatsCollection.models, function(beat, index){
var beatView = new Beat.View({container:'beat-container-'+this.model.idAttr, model:beat, idAttr:this.model.idAttr+'-'+index });
}, this);
return this;
},
removeRepresentation: function() {
console.log("Removing " + this.idAttr);
this.model.destroy();
this.remove();
},
//remove: function() {
// this.$el.remove();
//},
toggleRepType: function() {
console.log('Toggling ' + this.idAttr + ' type from ' + this.model.get('type'));
this.model.set('type', (this.model.get('type') == 'line' ? 'circle' : 'line'));
console.log('Toggled ' + this.idAttr + ' type to ' + this.model.get('type'));
this.trigger('rep:toggle');
},
addBeat: function() {
this.trigger('rep:addbeat');
},
removeBeat: function() {
this.trigger('rep:removebeat');
}
});
return representation;
});
This answer should be working properly for all views, being able to create, or delete views without effecting non related views, and change attributes and have related views auto update. Again, this is to use as a teaching example to show how to properly set up a backbone app without the zombie views...
Problem
The reason you are seeing duplicate views created lies in the render() function for the Beat's view:
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
}
This function is called when:
when the model associated with the view changes
the beat view is first initialized
The first call is the one causing the problems. initialize() uses an event listener to watch for changes to the model to re-render it when necessary:
initialize: function(options) {
...
this.model.on('change', this.render, this); // case #1 above
this.render(); // case #2 above
...
},
Normally, this is fine, except that render() includes code to push the view into the DOM. That means that every time the model associated with the view changes state, the view not only re-renders, but is duplicated in the DOM.
This seems to cause a whole slew of problems in terms of event listeners being bound incorrectly. The reason, as far as I know, that this phenomenon isn't caused when there is just one beat present is because the representation itself also re-renders and removes the old zombie view. I don't entirely understand this behavior, but it definitely has something to do with the way the representation watches it's beatCollection.
Solution
The fix is quite simple: change where the view appends itself to the DOM. This line in render():
$('#'+this.container).append(this.el);
should be moved to initialize, like so:
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
$('#'+this.container).append(this.el); // add to the DOM after rendering/updating template
},
Plnkr demo with solution applied
I have a measure model, that is made up of two collections, a beats collections, and a measureRep[resentation] collection. Each collection is made of beat models and representation models respectively.
Whenever a measureRep[resentation] collection changes (by addition or subtraction of a representation model), I want the measureView (which has the measure model, and therefore the measureRep[resentation] collection) to re-render itself using the render function().
I am adding a new model in another View by the following function:
var representationModel = new RepresentationModel({representationType: newRepType});
StageCollection.get(cid).get('measures').models[0].get('measureRepresentations').add(representationModel);
I can see that before and after the addition that the measure and its measureRep collection are getting added correctly, however, the bind call on the measureView is not registering the change and calling the render function. I even put the bind on the model, to show that the backend is getting updated, however, it doesn't respond. This leads me to believe that the View and the models are decoupled, but that doesn't make sense, since it originally renders from the model. Here are the relevant files:
measureView.js [View]:
define([...], function(...){
return Backbone.View.extend({
initialize: function(options){
if (options) {
for (var key in options) {
this[key] = options[key];
}
this.el = '#measure-container-'+options.parent.cid;
}
window.css = this.collectionOfRepresentations; //I use these to attach it to the window to verify that the are getting updated correctly
window.csd = this.model; // Same
_.bindAll(this, 'render'); // I have optionally included this and it hasn't helped
this.collectionOfRepresentations.bind('change', _.bind(this.render, this));
this.render();
},
render: function(){
// Make a template for the measure and append the MeasureTemplate
var measureTemplateParameters = { ... };
var compiledMeasureTemplate = _.template( MeasureTemplate, measureTemplateParameters );
// If we are adding a rep, clear the current reps, then add the template
$(this.el).html('');
$(this.el).append( compiledMeasureTemplate )
// for each rep in the measuresCollection
_.each(this.collectionOfRepresentations.models, function(rep, repIndex) {
var measureRepViewParamaters = { ... };
new MeasureRepView(measureRepViewParamaters);
}, this);
return this;
},
...
});
});
measure.js [Model]:
define([ ... ], function(_, Backbone, BeatsCollection, RepresentationsCollection) {
var MeasureModel = Backbone.Model.extend({
defaults: {
beats: BeatsCollection,
measureRepresentations: RepresentationsCollection
},
initialize: function(){
var logg = function() { console.log('changed'); };
this.measureRepresentations.bind('change', logg);
this.bind('change', logg);
}
});
return MeasureModel;
});
representations.js [Collection]:
define([ ... ], function($, _, Backbone, RepresentationModel){
var RepresentationsCollection = Backbone.Collection.extend({
model: RepresentationModel,
initialize: function(){
}
});
return RepresentationsCollection;
});
I have also tried registering the bind on the measure model, and not its child collection, but neither work.
_.bindAll(this, 'render');
this.model.bind('change', _.bind(this.render, this));
see: https://stackoverflow.com/a/8175141/1449799
In order to detect additions of models to a collection, you need to listen for the add event (not the change event, which will fire when a model in the collection is changed http://documentcloud.github.io/backbone/#Events-catalog ).
so try:
this.measureRepresentations.bind('add', logg);
I would like to say "when question_number changes, run updateQuestionNumber. If anything EXCEPT question_number (and whatever else I give a custom render function), run .render()".
The problem with the following code is that both updateQuestionNumber AND render run.
v.QuestionBuilder = Backbone.View.extend({
initialize: function() {
this.model.on('change:question_number', this.updateQuestionNumber, this);
this.model.on('change', this.render, this);
},
//only this function should run when question_number is changed
updateQuestionNumber: function(){
this.$('.question-number').text(this.model.get('question_number'));
},
//this should run when anything except question_number is changed
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.fadeIn('slow');
/* blah blah blah */
return this;
}
});
Here is the best solution I could think of:
When this.render() fires I set an array non_render of properties which don't require a full render (are taken care of by this.model.on('change:whatever', this.whateverRenderFunction, this);). Then it checks the changedAttributes. If there is anything in there that's NOT in non_render (._difference) then it continues. Otherwise it returns.
This seems like a super-bulky solution though (and I have to maintain the non_render list both in initialize and in render. Any improvements would be awesome...
render: function(model, value, options){
if(model){ //this was triggered via .change()
var changed = _.keys(model.changedAttributes());
var non_render = ['question_number'];
//if there is anything in changed that's not part of non_render
if(_.difference(changed, non_render).length === 0){
return false;
}
}
var data = this.model.toJSON();
_.extend(data, _.result(this.templateProperties));
this.$el.html(this.template(data));
}
initialize: function() {
this.model.on('change', function(model) {
var changed = model.getChanged();
if(changed && 1 === changed.length && changed.question_number) {
this.updateQuestionNumber();
} else {
this.render()
}
}, this);
},
I am trying to render a collection of items. Normally what I would do is something like this:
StuffView = Backbone.View.extend({
...
render: function(){
...
this.$el.html( ... );
return this;
}
...
});
StuffCollectionView = Backbone.View.extend({
...
render: function(){
this.collection.each(addOne, this);
},
addOne: function(stuff){
var view = new StuffView({model: stuff});
this.$el.append(view.render().el);
}
...
});
However, this time I'm building a bit different type of view. Each StuffView's rendering takes some time, so I can't do this synchronously. The code for the new StuffView looks something like this:
StuffView = Backbone.View.extend({
...
render: function(){
...
// Asynchronous rendering
SlowRenderingFunction(function(renderedResult){
this.$el.html(renderedResult);
});
}
});
In this case, I can't just return this from render and append its result to the StuffCollectionView's el. One hack I thought of was to pass a callback function to StuffView's render, and let it callback when it has finished rendering. Here's an example:
StuffView = Backbone.View.extend({
...
render: function(callback){
...
// Asynchronous rendering
SlowRenderingFunction(function(renderedResult){
this.$el.html(renderedResult);
callback(this);
});
}
});
StuffCollectionView = Backbone.View.extend({
...
initialize: function(){
_.bindAll(this, "onStuffFinishedRendering");
},
render: function(){
this.collection.each(addOne, this);
},
addOne: function(stuff){
var view = new StuffView({model: stuff});
view.render(onStuffFinishedRendering);
},
onStuffFinishedRendering: function(renderedResult){
this.$el.append(renderedResult.el);
}
...
});
But it's not working for some reason. Furthermore, this feels too hacky and doesn't feel right. Is there a conventional way to render children views asynchronously?
Can't you pass StuffCollectionView's el into the SlowRenderingFunction? It's a bit nasty but I don't see why it wouldn't work.
Edit: I should say, and make SlowRenderingFunction an actual property of StuffView, so that StuffViewCollection can call it instead of calling render.
You can try using _.defer to prevent the collection items rendering blocking the UI.
Refer http://underscorejs.org/#defer for more details.
StuffCollectionView = Backbone.View.extend({
...
render: function(){
var self = this;
_(function() {
self.collection.each(addOne, self);
}).defer();
}
...
});
Does anyone know which event is fired after a view is rendered in backbone.js?
I ran into this post which seems interesting
var myView = Backbone.View.extend({
initialize: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
var _this = this;
this.render = _.wrap(this.render, function(render) {
_this.beforeRender();
render();
_this.afterRender();
return _this;
});
},
beforeRender: function() {
console.log('beforeRender');
},
render: function() {
return this;
},
afterRender: function() {
console.log('afterRender');
}
});
Or you can do the following, which is what Backbone code is supposed to look like (Observer pattern, aka pub/sub). This is the way to go:
var myView = Backbone.View.extend({
initialize: function() {
this.on('render', this.afterRender);
this.render();
},
render: function () {
this.trigger('render');
},
afterRender: function () {
}
});
Edit: this.on('render', 'afterRender'); will not work - because Backbone.Events.on accepts only functions. The .on('event', 'methodName'); magic is made possible by Backbone.View.delegateEvents and as such is only available with DOM events.
As far as I know - none is fired. Render function is empty in source code.
The default implementation of render is a no-op
I would recommend just triggering it manually when necessary.
If you happen to be using Marionette, Marionette adds show and render events on views. See this StackOverflow question for an example.
On a side note, Marionette adds a lot of other useful features that you might be interested in.
I realise this question is fairly old but I wanted a solution that allowed the same custom function to be called after every call to render, so came up with the following...
First, override the default Backbone render function:
var render = Backbone.View.prototype.render;
Backbone.View.prototype.render = function() {
this.customRender();
afterPageRender();
render();
};
The above code calls customRender on the view, then a generic custom function (afterPageRender), then the original Backbone render function.
Then in my views, I replaced all instances of render functions with customRender:
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
this.model.fetch();
},
customRender: function() {
// ... do what you usually do in render()
}
Instead of adding the eventhandler manually to render on intialization you can also add the event to the 'events' section of your view. See http://backbonejs.org/#View-delegateEvents
e.g.
events: {
'render': 'afterRender'
}
afterRender: function(e){
alert("render complete")
},
constructor: function(){
Backbone.View.call(this, arguments);
var oldRender = this.render
this.render = function(){
oldRender.call(this)
// this.model.trigger('xxxxxxxxx')
}
}
like this http://jsfiddle.net/8hQyB/