PDA

View Full Version : Keeping the DOM small when using a TabPanel



t.hoepfner
1 Aug 2011, 5:02 AM
Hi,

I've spent many hours finding a way to keep the DOM small when using a TabPanel. Ideally, only the contents of the active tab should be kept in the DOM, except for the situation where one switches from tab A to tab B, where both A and B should be in the DOM until the switch is complete and the A should be removed from the DOM.

When I add a listener on 'cardswitch' and destroy the old card after the switch, then, well, it's gone. Not only from the DOM, but all together, so I cannot switch back to it later.

Sample code:



new Ext.TabPanel({
fullscreen: true,
listeners: {
scope: this,
'cardswitch': function(parent, newCard, oldCard, index, animated) {
console.log("cardswitch from ", oldCard.itemId, "to", newCard.itemId);
oldCard.destroy();
}
},
items: [
{
title: "Panel 1",
itemId: "panel_1",
html: 'Content of first panel'
},
{
title: "Panel 2",
itemId: "panel_2",
html: 'Content of second panel'
}
]
});


Could anyone point me in the right direction, please?

Thanks for your help,

Timo

simondoherty
1 Aug 2011, 5:32 AM
If you destroy it, you will need to recreate the panel / content from scratch. This includes any other components you have included on the panel unless you specify the autoDestroy property to be false.

The simplest way to recreate the panel is to define the panel as a global variable and put a constructor in a method that you can call each time it is needed.

You might also wish to consider the amount of time it will take to recreate a complex panel and made a decision as to whether to have a larger DOM but reduced loading time or a longer load time but smaller DOM.

Hope this helps.

mitchellsimoens
1 Aug 2011, 7:32 AM
You need to be careful with global variables, can be forgotten and a memory leak.

What I do is each tab is a Panel and when the activate event is fired I add it's items. On deactivate I remove the items. I don't use cardswitch because not every tab is the same, I may want to do some action on one tab that another doesn't need. Areguably you could have the same method on each view but I have everything in a Controller.

When I create a Panel, I only have the activate event with the option of single as true. When that event fires, it removes itself and I add the deactivate event which will remove itself and add the activate event.

So, let's say you have a List in a Panel and if you destroy it, chances are you are probably going to destroy the Store. To combat that, I put the Store reference onto the Panel and when I add the items (the List) I just have to put the Store on the List from the reference on the Panel.

Just what I have learned works great for me.

t.hoepfner
1 Aug 2011, 8:28 AM
Thanks a lot to both of you, I think this brought me back on the right track. The add/remove approach seems to work fine for me. Here's some snippet I used for testing:



new Ext.Application({
launch: function() {

var lazyPanel = Ext.extend(Ext.Panel, {
lazyItems: null,
initComponent : function() {
this.lazyItems = this.items;
this.items = [];
this.superclass().initComponent.apply(this);
},
listeners: {
activate: function() {
console.log("activate", this.xtype, this.id);
this.add(this.lazyItems);
this.doComponentLayout();
},
deactivate: function() {
console.log("deactivate", this.xtype, this.id);
this.removeAll(true);
}
}
});
Ext.reg('lazy', lazyPanel);

var viewport = new Ext.TabPanel({
fullscreen: true,
items: [
{
xtype: 'lazy',
title: "Panel 1",
items: [
{
html: 'Content 1',
listeners:{
afterrender:function() {
console.log('afterrender', this.xtype, this.id)
}
}
}
]
},
{
xtype: 'lazy',
title: "Panel 2",
items: [
{
html: 'Content 2',
listeners:{
afterrender:function() {
console.log('afterrender', this.xtype, this.id)
}
}
}
]
}
],
listeners: {
cardswitch: function(panel, newCard, oldCard) {
console.log("cardswitch", this.xtype, oldCard.xtype, oldCard.id, "->", newCard.xtype, newCard.id);
}
}
});
}
});


Mitchell, I'n not sure I understand why you add the deactivate listener in the activate event and vice versa instead of adding both right away?

It seems that when using a card switch animation and switching cards, the new card is not yet rendered, so you see that the old card flies out but an empty card flies in. Immediately after the animation is complete, the content of the new card then appears. I'll take a look into that tomorrow.

Thanks again for the help!

Timo

mitchellsimoens
1 Aug 2011, 8:32 AM
Mitchell, I'n not sure I understand why you add the deactivate listener in the activate event and vice versa instead of adding both right away?

Just like keeping the DOM lean and mean, I keep the listeners lean and mean. When a Panel is activated I don't need the activate event and when the Panel is deactivated I don't need the deactivate event. I just go to as much trouble I can to keep things as small as possible. You do have to watch out or you will be doing to much trying to keep things small and that is then defeating the purpose.

t.hoepfner
1 Aug 2011, 8:58 AM
Thanks Mitchell, that makes sense now.

Here's the modified part of the example code in case someone wants to follow along:



var lazyPanel = Ext.extend(Ext.Panel, {
lazyItems: null,
initComponent : function() {
this.lazyItems = this.items;
this.items = [];
this.on('activate', this.handleActivate, this);
this.superclass().initComponent.apply(this);
},
handleActivate: function() {
console.log("activate", this.xtype, this.id);
this.un('activate', this.handleActivate, this);
this.on('deactivate', this.handleDeactivate, this);
this.add(this.lazyItems);
this.doComponentLayout();
},
handleDeactivate: function() {
console.log("deactivate", this.xtype, this.id);
this.un('deactivate', this.handleDeactivate, this);
this.on('activate', this.handleActivate, this);
this.removeAll(true);
}
});
Ext.reg('lazy', lazyPanel);


Timo

mitchellsimoens
1 Aug 2011, 9:09 AM
Simpler:


var lazyPanel = Ext.extend(Ext.Panel, {
lazyItems: null,
initComponent : function() {
this.lazyItems = this.items;
this.items = [];
this.on('activate', this.handleActivate, this, { single : true });
this.superclass().initComponent.apply(this);
},
handleActivate: function() {
console.log("activate", this.xtype, this.id);
this.on('deactivate', this.handleDeactivate, this, { single : true });
this.add(this.lazyItems);
this.doComponentLayout();
},
handleDeactivate: function() {
console.log("deactivate", this.xtype, this.id);
this.on('activate', this.handleActivate, this, { single : true });
this.removeAll(true);
}
});
Ext.reg('lazy', lazyPanel);

Now Sencha Touch will remove the events after firing once.

t.hoepfner
1 Aug 2011, 9:14 AM
Even better! I forgot about the "single" option for the events.

Thanks a lot!

Timo

nicholasgins
1 Aug 2011, 2:43 PM
This is exactly what I am looking for. Do you think it is possible to have the full example with the items? Currently it says this.items = []; I figure adding the items to the array will work but a mistake will lead to a longer thread.

I believe that the removal of DOM elements make the UI much lighter and performance is much greater. My current app loads the HTML needed via AJAX.Request and that is a huge performance help as the user only loads what content they need when they need it but I what has been loaded in the Store. The next part of the performance of the app is the removal of the DOM elements as they are no longer needed and the creation of the elements as they become activated.

Thank you for your help,

Nick

t.hoepfner
2 Aug 2011, 12:35 AM
Hi Nick,

here's a more complete example with code comments.



<!DOCTYPE html>
<html>
<head>
<title>Keeping the DOM small when using a TabPanel</title>
<link href="../lib/touch/resources/css/sencha-touch.css" rel="stylesheet" type="text/css"/>
<script src="../lib/touch/sencha-touch-debug-w-comments.js" type="text/javascript"></script>

<script type="text/javascript" charset="utf-8">
TestApp = new Ext.Application({
name: 'TestApp',
launch: function() {
this.views.viewport = new TestApp.views.Viewport();
}
});

TestApp.views.Viewport = Ext.extend(Ext.TabPanel, {
fullscreen: true,
items: [
{
xtype: 'lazy-panel', // lazy-panel is defined below
title: "Panel 1",
items: [
{ html: 'Content 1' }
]
},
{
xtype: 'lazy-carousel', // lazy-carousel is defined below
title: "Panel 2",
items: [
{ html: 'Content 2a' },
{ html: 'Content 2b' },
{ html: 'Content 2c' }
]
}
]
});

// Subclass of Ext.Panel that adds its children on activate and
// removes them on deactivate to keep the DOM lean.
TestApp.views.LazyPanel = Ext.extend(Ext.Panel, {
lazyItems: null,
initComponent : function() {
// Store a reference to the children of this panel in a variable unknown to its superclass
this.lazyItems = this.items;
// Prevent the superclass of rendering the children by supplying an empty list
this.items = [];
// Register handler that will add the children on activate
this.on('activate', this.handleActivate, this, { single : true });
// The rest is handled by the superclass
this.superclass().initComponent.apply(this);
},
handleActivate: function() {
console.log("activate", this.xtype, this.id);
// Add the children
this.add(this.lazyItems);
// Make sure everything is rendered
this.doComponentLayout();
// Register handler that will remove the children on deactivate
this.on('deactivate', this.handleDeactivate, this, { single : true });
},
handleDeactivate: function() {
console.log("deactivate", this.xtype, this.id);
// Remove all children
this.removeAll(true);
// Register handler that will add the children on activate
this.on('activate', this.handleActivate, this, { single : true });
}
});
// Register xtype for LazyPanel
Ext.reg('lazy-panel', TestApp.views.LazyPanel);

// Subclass of Ext.Carousel that adds its children on activate and
// removes them on deactivate to keep the DOM lean.
TestApp.views.LazyCarousel = Ext.extend(Ext.Carousel, {
lazyItems: null,
initComponent : function() {
this.lazyItems = this.items;
this.items = [];
this.on('activate', this.handleActivate, this, { single : true });
this.superclass().initComponent.apply(this);
},
handleActivate: function() {
console.log("activate", this.xtype, this.id);
this.add(this.lazyItems);
this.doComponentLayout();
this.on('deactivate', this.handleDeactivate, this, { single : true });
},
handleDeactivate: function() {
console.log("deactivate", this.xtype, this.id);
this.removeAll(true);
this.on('activate', this.handleActivate, this, { single : true });
}
});
// Register xtype for LazyCarousel
Ext.reg('lazy-carousel', TestApp.views.LazyCarousel);

</script>
</head>
<body>
</body>
</html>


There are still some problems with the code. E.g. if you make the carousel the first item of the TabPanel, it will render empty until you move to the second tab and then come back to the first tab. Or when using page transitions, the target card is empty during the animation. I'll look into that, but don't hold you breath...

Timo

fx-mike
3 Aug 2011, 4:30 AM
...
Or when using page transitions, the target card is empty during the animation.
...


How about using beforeactivate? As far as I can tell by looking at the CardLayout source, it is called before the card transition, while activate is obviously called just after the transition.

Thanks for the sample code, I will give it a try in my app as soon as I get the chance.

Cheers

mitchellsimoens
3 Aug 2011, 4:47 AM
In a TabPanel, the first tab should fire the activate event, if not that is a bug and then you can render the first tab with items to get around it.

As for having things empty during transitions... good luck keeping things smooth if you render too much.

headkit
2 Dec 2011, 6:27 AM
*listening*