Skip to content

Commit 356314a

Browse files
authored
Dispose tabs on Select transition (#12)
Select.hideCurrent now removes the outgoing tab from the DOM and disposes its component (firing exitDocument and disposeInternal), instead of just calling .hide() and leaving an inactive but live component in the document. This fixes unbounded DOM growth in long-running SPA sessions where users browse hundreds of routes and every previously-visited tab stays cached in the parent's content element with display:none.
1 parent a3b2761 commit 356314a

3 files changed

Lines changed: 31 additions & 16 deletions

File tree

js/ui/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ closure_js_library(
8888
"@stackb_rules_closure//closure/goog/dom:dataset",
8989
"@stackb_rules_closure//closure/goog/dom:tagname",
9090
"@stackb_rules_closure//closure/goog/events:event",
91+
"@stackb_rules_closure//closure/goog/ui:component",
9192
],
9293
)
9394

js/ui/route.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,6 @@ class Route extends EventTarget {
140140
return this.path_.slice(0, this.index_);
141141
}
142142

143-
// /** Peek at the next path segment.
144-
// * @return {string}
145-
// */
146-
// next() {
147-
// return this.path_[this.index_ + 1];
148-
// }
149-
150143
/** Get the current path segment.
151144
* @return {string}
152145
*/

js/ui/select.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
goog.module('stack.ui.Select');
55

6+
const ComponentEventType = goog.require('goog.ui.Component.EventType');
67
const TabEvent = goog.require('stack.ui.TabEvent');
78
const Template = goog.require('stack.ui.Template');
89
const asserts = goog.require('goog.asserts');
@@ -97,7 +98,12 @@ class Select extends Component {
9798
showTab(name) {
9899
var tab = this.getTab(name);
99100
if (tab) {
100-
this.hideCurrent();
101+
// Don't tear down the current tab when re-selecting it (e.g.
102+
// navigating from /modules/foo/0.5.4 to /modules/foo/0.5.5 — at the
103+
// outer BodySelect level the tab name is still "modules").
104+
if (this.current_ !== name) {
105+
this.hideCurrent();
106+
}
101107
this.current_ = name;
102108
//var path = this.getPath();
103109
//path.push(name);
@@ -227,19 +233,34 @@ class Select extends Component {
227233
}
228234

229235
/**
230-
* Hide the current tab and make it the previous.
236+
* Hide the current tab and make it the previous. The outgoing tab is
237+
* removed from this Select's children, detached from the DOM, and
238+
* disposed — so its component lifecycle reaches `exitDocument` and
239+
* `disposeInternal`, releasing event handlers and any custom state.
240+
* Subscribers that key off the tab name (e.g. SelectNav highlights) get
241+
* a final HIDE event before the element goes away.
242+
*
243+
* Tabs are recreated on demand by `selectFail` if the user navigates
244+
* back to them.
245+
*
231246
* @return {?Component}
232247
*/
233248
hideCurrent() {
234-
var prev = null;
235-
if (this.current_) {
236-
this.prev_ = this.current_;
237-
prev = this.getTab(this.prev_);
238-
if (prev) {
239-
prev.hide();
240-
}
249+
if (!this.current_) {
250+
return null;
241251
}
252+
const name = this.current_;
253+
const prev = this.getTab(name);
254+
this.prev_ = name;
242255
this.current_ = null;
256+
if (prev) {
257+
// Notify listeners (e.g. SelectNav) before the element is gone.
258+
prev.dispatchEvent(ComponentEventType.HIDE);
259+
// removeChild(c, true) detaches the element and fires exitDocument.
260+
this.removeChild(prev, true);
261+
delete this.name2id_[name];
262+
prev.dispose();
263+
}
243264
return prev;
244265
}
245266

0 commit comments

Comments
 (0)