Tabbed UI demo annotated source

Back to index

        

The tabs sample project demonstrates the Torus Router and Component.from(). All of the state here is kept within the views for demonstration purposes, but should probably be moved to a Record under the App instance in practice for simplicity.

5

Bootstrap the required globals from Torus, since we're not bundling

7for (const exportedName in Torus) {
8    window[exportedName] = Torus[exportedName];
9}
10

This is a single tab page. Because we want tab contents to be long-lived, we define it as a function, but make a class component out of it with Component.from() This makes a class component that can be constructed with the arguments number, content, whose compose function produces the given DOM.

15const Tab = Component.from((number, content) => {
16    return jdom`<div>
17        <h2>Tab #${number}</h2>
18        <p>${content}</p>
19    </div>`;
20});
21

The tab buttons are nav buttons to switch between tabs using the Torus router. Because it's such a simple component, we just write it as a function to reuse in App.

24const TabButton = (number, active) => {

We can tell the router to go to a specific location with Router#go().

26    return jdom`<button style="background:${active ? '#555' : '#fff'};color:${active ? '#fff' : '#000'}"
27        onclick="${() => router.go(`/tab/${number}`)}">Switch to tab #${number}
28    </button>`;
29}
30

The app contains all 3 tabs and a row of tab buttons.

32class App extends StyledComponent {
33
34    init(router) {

We want to keep the tabs around even if they aren't visible, so we create them here.

36        this.tabs = [
37            new Tab(0, 'The first tab\'s content is pretty bland, nothing special here.'),
38            new Tab(1, 'The second tab is a bit more interesting, but it\'s really nothing of substance.'),
39            new Tab(2, 'The third tab embarks on a dazzling discourse of human fallacies.'),
40        ];
41

By default the active tab is the 0th tab.

43        this.setActiveTab(0);
44

Rather than binding this component to some model, we bind it to the router. This means every time the URL changes, an event will fire with the name we gave the matching route, and any parameters we gave the route.

48        this.bind(router, ([name, params]) => {
49            switch (name) {
50                case 'tab':
51                    this.setActiveTab(params.tabNumber);
52                    break;
53                default:

If no routes match, let's make tab 0 active

55                    this.setActiveTab(0);
56                    break;
57            }

This is also the right place to set the document title based on the route.

59            document.title = `Tab ${params.tabNumber || 0} | Torus Tabbed UI`;
60        });
61    }
62
63    styles() {
64        return {
65            'font-family': 'system-ui, sans-serif',
66        }
67    }
68
69    setActiveTab(tabNumber) {

this.activeTab will always point to the current active tab component

71        this.activeTab = this.tabs[tabNumber];
72        this.render();
73    }
74
75    compose() {
76        return jdom`
77            <main>
78                <h1>Tabbed View</h1>
79                <ul>${this.tabs.map((tab, number) => {
80                    return TabButton(number, tab === this.activeTab)
81                })}</ul>
82                ${this.activeTab.node}
83            </main>
84        `;
85    }
86
87}
88

We define the app's router here, by giving it a dictionary of the routes we want, keyed by unique names.

91const router = new Router({
92    tab: '/tab/:tabNumber',
93    default: '/',
94});
95

Create the app instance, which we define earlier to be called with a router, and mount it to the DOM.

98const app = new App(router);
99document.body.appendChild(app.node);
100