I have a LayoutView that handles showing it's own children. According to the documentation, I can extend the childEvents object to automagically listen for any events triggered on it's child views.
This isn't working for me at all. Can anyone spot what I might be doing wrong?
Show.QuizLayout = Marionette.LayoutView.extend({
template:_.template("<h3><%= title %></h3><div id='quiz'></div>"),
regions: {
card: "#quiz",
},
childEvents: {
"next:question": "showNextQuestion",
},
showNextQuestion:function () {
//I NEVER GET CALLED!!!
},
onShow:function(){
var v = new Show.QuizCard();
this.showChildView('card',v)
});
Show.QuizCard = Marionette.ItemView.extend({
className: "quizcard",
template: _.template("<div class='card' id='next'>Next</div>"),
events: {
"click #next":function(e){
this.trigger("next:question")
}
},
});
I have gotten around this by setting v.on("next:question", function(){...}), but it creates cruft I'd rather not have to deal with, if I could get childEvents to work the way they're supposed to.
http://marionettejs.com/docs/v2.4.5/marionette.layoutview.html#layoutview-childevents
As noted in the docs, you should use triggerMethod within your childview, rather than trigger!
childEvents also catches custom events fired by a child view. Take
note that the first argument to a childEvents handler is the child
view itself. Caution: Events triggered on the child view through
this.trigger are not yet supported for LayoutView childEvents. Use
strictly triggerMethod within the child view.
Try:
childEvents: {
"next:question": this.showNextQuestion,
}
Or see if you have better luck with triggerMethod
events: {
"click #next":function(e){
this.triggerMethod("next:question")
}
}
Then in your layout have a method called onChildviewNextQuestion
Related
I am trying to make my very first search app.
After the app is built, every DOM is rendering as I expect and events work as well. When I dig deeper into it, I find a strange behavior, and after I did some search, I found it is because of zombie view events delegate issue.
Here is some part of my code:
var searchList = Backbone.View.extend({
events:{
'click #submit':function() {
this.render()
}
},
render() {
this.showList = new ShowList({el:$('.ADoM')});
}
});
When #submit is clicked, a new instance of ShowList will be created and '.ADoM' DOM will be rendering.
showList.js
var showList = Backbone.View.extend({
events: {
"click .testing": function(e) {
console.log(e.currentTarget);
},
},
initialize() {
this.$el.html(SearchListTemplate());
}
});
The '.testing' button event is bound with it.
So as what 'zombie' does, after multiple clicks on submit, then clicking the '.testing' button, console.log() will output multiple time.
I have followed the article here and tried to understand and fix my issue, and also tried to add this.remove() in showList.js as someone mentioned, but unfortunately it may because I was not able to place them in the proper place in my code, the issue is still unsolved.
That has nothing to do with ES6, this is basic JavaScript and DOM manipulation.
Do not share the same element in the page
You're creating new ShowList instances which are bound to the same element in the page. In Backbone, that's bad practice.
Each Backbone View instance has its own root element on which events are bound. When multiple views share that same element, events are triggered on each instance, and you can't call remove on the view since it will remove the DOM element from the page completely.
You should dump the child view root element within the element you wish to reuse.
this.$('.ADoM').html(this.showList.render().el);
Reusing the view
The render function should be idempotent.
var searchList = Backbone.View.extend({
events: {
// you can use a string to an existing view method
'click #submit': 'render'
},
initialize() {
// instanciate the view once
this.showList = new ShowList();
},
// This implementation always has the same result
render() {
this.$('.ADoM').html(this.showList.render().el);
// Backbone concention is to return 'this' in the render function.
return this;
}
});
Your other view could be simplified as well to reflect the changes from the parent view.
var showList = Backbone.View.extend({
events: {
"click .testing": 'onTestingClick',
},
// Don't render in the initialize, do it in the render function
render() {
this.$el.html(SearchListTemplate());
},
onTestingClick(e) {
console.log(e.currentTarget);
}
});
This is a super basic example on reusing a view instead of always creating a new one.
A little cleanup is necessary
When done with a view, you should call remove on it:
Removes a view and its el from the DOM, and calls stopListening to
remove any bound events that the view has listenTo'd.
For this to work, when registering callbacks on model or collection events, use listenTo over on or bind to avoid other memory leaks (or zombie views).
A good pattern for view having multiple child views is to keep a reference of each child views and call remove on each of them when the parent gets removed.
See how to avoid memory leaks when rendering list views. When dealing with a lot of views (big list or tree of views), there are ways to efficiently render with Backbone which involves DocumentFragment batching and deferring.
I have a backbone page which behaves as follows.
Collection - > Models -> Views
In that I have a collection which contains search results of N length. Each of these models is tied to an instance of a view which in this case is a row of data being displayed.
I want to be able to toggle each row from their 'details' view to their 'advanced' view which contains more information. At the moment I have the parent view rendering N number of views for each model. I can toggled the change in state by updating the model and listening to the change event and then only re-rendering the part of the view I have clicked on. I noticed a problem whilst doing this. The problem is the viewport jump to the top of the page which isn't great UX.
Whilst debugging this I noticed something strange
The parent view's (page that holds the search results) render function is being called, which in turn is calling the render function of each of the rows. I think this is what's causing each of the child views to re-render.
Here are some code examples to demonstrate the problem:
// The child view's render and initialis
var SearchInviteRow = Backbone.View.extend({
tagName: "invite-section-result-row",
initialize: function(params){
this.template = templateHTML;
this.extendedTemplate = extendedTemplate;
this.listenTo(this.model, 'change', this.render);
},
events : {
"click .toggle-attachments" : "renderWithAttachments"
},
render: function(){
var view = this, render;
var that = this;
if(!this.model.get("expand")){
var rendered = Mustache.render(view.template, that.model.toJSON());
this.$el.html(rendered);
} else {
var rendered = Mustache.render(view.extendedTemplate, that.model.toJSON());
this.$el.html(rendered);
}
return this;
},
close: function(){
this.remove();
},
renderWithAttachments: function(){
if( !this.model.get("expand") ) {
this.model.set( {"expand" : true } );
this.model.getAttachments();
} else {
this.model.set( {"expand" : false } );
}
}
});
This is the part of the parent's render that iterates over the collection appending the rows to the search tiles.
for (var i = 0; i < this.collection.length; i++) {
view.subViewArray[i] = new SearchInviteRow({
model: this.collection.at(i)
});
this.$(".invite-section-inside").append(view.subViewArray[i].render().el);
}
What I can't work out is why the parent's render function is being called which is causing my other problems.
There are two ways this can happen, one is if you are listening to the backbone events on your collection the second is if it's a DOM event that's being triggered.
With the backbone events, if you look at the documentation you will see that events triggered on a model in a collection will also be triggered on the collection itself
Any event that is triggered on a model in a collection will also be
triggered on the collection directly, for convenience. This allows you
to listen for changes to specific attributes in any model in a
collection, for example: documents.on("change:selected", ...)
That being the case, you should either check in the events callback whether you want to act on it (see what triggered it, perhaps passing in a extra flag when triggering the event ), or make sure you are only listening to the specific events you are interested in acting on. For example instead of listening to you collections generic change event you might want to listen to the more specific version of it (change:someProperty).
With DOM events, this can easily happen if you are listening to the same selector in your parents view as in your child's view, since the parent's root el is also a parent to the child's el.
Consider the following JS:
var ChildLayout = Marionette.Layout.extend({
template: "... let's not go here ...",
initialize: function() {
console.log('Child Layout Initialized'); // registers once
},
onRender: function() {
console.log('Rendered'); // registers 2 times
},
});
var ParentLayout = Marionette.Layout.extend({
template: "<div class='child-region'></div>",
regions: { childRegion: '.child-region' },
onRender: function() {
console.log('About to initialize ChildLayout'); // registers once
this.childRegion.show(new ChildLayout());
},
});
In the above, I use the ParentLayout to render the ChildLayout in one of its Regions. Notice that I do not pass any sort of model to the ChildLayout.
The show function, a property of Marionette, should logically initialize and then render the model once. A Marionette View should not re-render itself unless there is some change in its model, from what I understand.
In my application, the onRender of ChildLayout is triggering in my code several times, though its initialize only triggers once.
I cannot see what is causing Marionette to render the ChildLayout multiple times - this does not make sense.
Any insight?
edit
On inspecting the source code of Marionette.js, the show function clearly only renders the passed view once, right after initializing. So the re-renders could only occur from the Layout deciding autonomously to re-render. Interesting.
This turned out to be my own problem with a Zombie View, which I since handled.
From the Router, I create a Search View with the following code:
(The if is to only create a new View where one does not already exist, since the View never changes.)
search: function () {
if (!this.searchView) {
this.searchView = new SearchView();
}
$('#content').html(this.searchView.el);
this.headerView.selectMenuItem('search-menu');
},
In the View, I have an events hash, binding a click event to a search button:
events: {
"click .search-query": "search"
},
This results in the event only firing the first time the search button is used.
Removing the if from the search function solves the problem.
This however does not seem to be the correct way to approach this, since the View should not need to be recreated (reinitialized and re-rendered).
An attempted fix:
I attempted to add this.delegateEvents(); to the render function of the Search View as follows (while leaving the if in the search function), but it did not solve the problem:
render:function () {
console.log('rendering search...');
$(this.el).html(this.template());
this.delegateEvents();
return this;
},
Any suggestions would be appreciated.
Since you're not calling render() again, delegateEvents won't be called again. You can just call delegateEvents directly in your router. This is just an example; there are probably more elegant ways to do it.
delegateEvents basically rebinds the events to your view. This is useful if the html of your views gets removed from the dom. That looks like what is happening in your example.
search: function () {
if (!this.searchView) {
this.searchView = new SearchView();
}
$('#content').html(this.searchView.el);
this.searchView.delegateEvents();
this.headerView.selectMenuItem('search-menu');
}
Consider the following example class Parent:
Ext.define('Parent', {
...
listeners: {
render: {
fn: doSomething
},
},
};
and the following class Child extending default Parent above:
Ext.define('Child', {
extend: 'Parent',
...
listeners: {
afterrender: {
fn: doSomething
},
},
};
Even though Child does not specify a listener for render (it only provides for afterrender) the render listener (defined in the Parent class) is not fired anymore upon Child's component rendering; i.e. the listener is overwritten by the new listeners specification.
How to fix this?
You can specify the handler in initComponent, instead of using the listeners config object.
Ext.define('Child', {
extend: 'Parent',
...
initComponent: function() {
this.on('afterrender', this.onAfterRender);
},
onAfterRender: function() {
...
}
};
The reason that the listeners config method doesn't work is because the config object that is passed to Ext.define gets Ext.apply'd to any new objects that are created. In other words, it completely overwrites any reference types (e.g. the listeners object).
Using initComponent is preferred, since it's specifically designed to be overridden for subclassing.
Well, there is no clean solution in the framework at the moment; however something a bit kludgy like this can be used relatively safe:
Ext.define('Child', {
extend: 'Parent',
listeners: Ext.merge(Parent.prototype.listeners || {}, {
...
})
});
I must admit that it's not much better than using initComponent but at least it's a bit more declarative.
"Fixed" issue by declaring the event handler within initComponent using on. So the sample code for the child class would be:
Ext.define('Child', {
extend: 'Parent',
...
initComponent : function(args) {
var me = this;
me.callParent(args);
me.on('afterrender', me.doSomething, this);
},
};
Still doesn't look that nice to me; if anyone has a better solution, please answer the question.
In version 6.02 configuring viewConfig with a new method in a child, will still keep other methods within viewConfig from the parent. Really good.