After an upgrade from version 1.8.8 to 2.4.1 my dynamic tabs control broke. The new Marionette won't let me do something that the old Marionette would.
A JQuery UI tab control expects a certain UI structure. At the core, it essentially wants an unordered list inside a container. Like this:
<div id="tabs-container">
<ul id="tabs">//this guy should have li's inside it. They'll become tabs.
</ul>
<div id="tabs-content">
</div>
</div>//end #tabs-container
If you call $("tabs-container").tabs() in your JavaScript, it will turn all of that stuff into a tab control with the li items inside the #tabs ul becoming your tab headers. Here's the key. It doesn't want the li items to be wrapped by anything, like a div. It wants the li's directly inside the ul.
Marionette wants to wrap stuff. And that's my problem. If I have a view render those li's inside the ul, the view wants to wrap it's output in a div. That messes things up.
In the old Marionette I used the view's "el" property to dump the view's output directly into the ul. Like this:
var TabCollectionView = Marionette.CollectionView.extend({
childView: TabChildView,
el: $("#tabs")
});
The new marionette doesn't like that. It says that's putting the parent (the #tabs container) inside the new child element when it goes to appendChild. Here's the error:
"Uncaught HierarchyRequestError: Failed to execute 'appendChild' on 'Node': The new child element contains the parent."
I'd like to rework this the right way for the new Marionette but am a bit stuck. How do I get closer to where I need to be?
UPDATE: In studying this a bit I see that I could pull the ul inside the view generation. So I wouldn't define the ul as a region into which a collection of li's is rendered. Then the problem becomes what DOM element will assume the role of region containing my tabs control. I'm wondering if this is a use case for the "Layout" component and if this would be a more solid approach.
If you think about it, a tab control is really two regions - one for the tab headers and one for the content divs associated with each tab. Two related regions requiring coordinated construction and display? A job for a Layout? Am I thinking right here? I'm looking to organize this optimally in solving it.
MORE ANALYSIS:
What I'm doing up to this point is placing a tabs container in my DOM and then dynamically rendering the tabs in it. Maybe what I should be doing is creating the entire tabs control, container and all, dynamically. In walking down that path there are basically three tasks that have to be completed:
- .tabs() has to be called on some container. (when?)
- A container has to be built. It must have two regions in it: a #ul region for tab headers and a separate #div region for tab contents.
- Collection views for both tab headers and and tab content have to be rendered
The questions that come out of that for me are:
- How can I know when the two regions have been shown so that I can call .tabs() on their container? onShow() maybe works for something like this where you have one CollectionView, but I need to know when two two CollectionViews have been shown.
- How do I tell some structure (the outer tabs container) to render itself, and then to render two regions? Is this what Marionette Layout does?
I'm just thinking out loud here really. I'm reading about Layout now to see if this is a fit. Looking for an indication that I'm heading down the right path.
NEW CODE UPDATE: closer but still not working. DOM structure rendered seems ok. But tabs() initialization doesn't work. Doing it on the fly during the construction of a LayoutView just doesn't seem to work. The tabs css classes don't get added completely and the result looks nothing like a tab control. I don't think this LayoutView approach will work with JQuery UI tabs. I think what may be needed is to go back to the original approach, a ul region for tabs and another region for the content areas - the approach that used to work in Marionette 1.x. Maybe there is a way to remove the wrapper around the collections. It's that extra wrapper that is breaking the control. I can't think of another way to do this.
var TabLayoutView = Backbone.Marionette.LayoutView.extend({
id: "tab-layout",
template: Handlebars.compile(
'<ul id="tabs">' +
'</ul>' +
'<div id="tab-content">' +
'</div>'
),
regions: {
tabsRegion: '#tabs',
tabContentRegion: "#tab-content"
},
onBeforeShow: function () {
var tabsRegion = this.getRegion('tabsRegion');
var tabCollectionView = new TabCollectionView({
collection: this.collection
});
var tabContentRegion = this.getRegion('tabContentRegion');
var tabContentCollectionView = new TabContentCollectionView({
collection: this.collection
});
tabsRegion.show(tabCollectionView);
tabContentRegion.show(tabContentCollectionView);
$('#tab-layout').tabs(); //Applies the tabs styling to the outer container (TabLayoutView)
}
});
//The tabs up top
var TabItemView = Marionette.ItemView.extend({
tagName: "li",
template: Handlebars.compile(
'<a href="#tabs-{{id}}"><img src="../icon.png"></img> {{tabName}}</a>'
)
});
var TabCollectionView = Marionette.CollectionView.extend({
childView: TabItemView,
//el: $("#tabs") //Setting el like this used to work in Marionette js version 1.x. Commented out now.
});
//The tab content areas below the tabs
var TabContentItemView = Marionette.ItemView.extend({
tagName: "div",
template: Handlebars.compile(
'<div id="tabs-{{id}}">' +
'</div>'
)
});
var TabContentCollectionView = Marionette.CollectionView.extend({
childView: TabContentItemView,
//el: $("#tab-content") //Setting el like this used to work in Marionette js version 1.x. Commented out now.
});
Above produces output below:
<div id="tabs-container">
<div id="tab-layout" class="ui-tabs ui-widget ui-widget-content ui-corner-all">
<ul id="tabs" class="ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" role="tablist">
<div>
<li>
<a href="#tabs-4689e60e-1f96-4189-b0a2-6e8c4a2e6822"><img src="img.png"> Bob Jones </a>
</li>
</div>
</ul>
<div id="tab-content">
<div>
<div>
<div id="tabs-4689e60e-1f96-4189-b0a2-6e8c4a2e6822">
</div>
</div>
</div>
</div>
</div>
</div>
To build a collection view without div wrappings to produce:
you only need to do this:
Perhaps doing this and not needing to handle the other scenario will fix your problem?
I don't know if this will jive with your design concept, but you may consider making
#tabs-container
a region, in which, you would.show()
This will replicate your DOM.
If you can't do this and you want to drop a list into the
ul#tabs
region, I can show you a hack to get around theCollectionView
wrapper. Drop me a comment.UPDATE A LayoutView would solve all your problems.
region
property would find the element in your template you want to use as a container, in this caseYou'd use this region to fever you CollectionView, for example. Like this:
Using the MyListView we defined above.
onAttach
callback to ensure that your html is in the DOM. This callback was designed specifically to work with plugins that depend on elements existing in the DOM. Read here and see that entire section. Something like,Where
#tabs
is a child of#tabs-region
, is theul
you rendered in that region.UPDATE In the comments below the OP explained that he needs to know when both views are rendered. In general
Region.show()
is a synchronous process and thus, once both views are rendered we simply invokeFrom the LayoutView. Like this,
If it happens that the views render asynchronously, then we would attach a listener to the views using the same handler. We would check the handler and once both views have checked in, we'd call
this.$el.tabs()
.UPDATE The OP is looking for this DOM output:
To do that we need to set up the views as follows:
Note: Read the above carefully the changes to your original code are minor, but are commented.