Slide deck demo annotated source

Back to index

        

Slides framework demo. This showcases a super simple slide deck library for the web, written as Torus components (mostly function components).

4

Since this is more of a library than an app, there's going to be lots of unused vars

7/* eslint no-unused-vars: 0 */
8

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

10for (const exportedName in Torus) {
11    window[exportedName] = Torus[exportedName];
12}
13

In-slide components

15
16const Title = (content = 'Title') => {
17    return {
18        tag: 'h1',
19        children: [content],
20    }
21}
22
23const Subtitle = (content = 'Subtitle') => {
24    return {
25        tag: 'h2',
26        attrs: {
27            class: 'subtitle',
28        },
29        children: [content],
30    }
31}
32
33const Paragraph = content => {
34    return {
35        tag: 'p',
36        attrs: {
37            style: {
38                width: '100%',
39            },
40        },
41        children: [content],
42    }
43}
44

Internal List implementation. BulletList and NumberedList compose this.

47const List = (type, children) => {
48    return {
49        tag: type,
50        attrs: {
51            style: {
52                width: '100%',
53            },
54        },
55        children: children.map(comp => {
56            return {
57                tag: 'li',
58                children: [comp],
59            }
60        }),
61    }
62}
63
64const BulletList = (...children) => {
65    return List('ul', children);
66}
67
68const NumberedList = (...children) => {
69    return List('ol', children);
70}
71

An Image component, with alt text and some basic styles

73const Image = ({
74    src = '#',
75    alt = 'Image placeholder',
76} = {}) => {
77    return {
78        tag: 'img',
79        attrs: {
80            src: src,
81            alt: alt,
82            style: {
83                maxWidth: '100%',
84                maxHeight: '100%',
85                boxShadow: '0 3px 6px rgba(0, 0, 0, .3)',
86            },
87        },
88    }
89}
90

Row and Column are flexbox-type containers that align the children vertically or horizontally. As a general pattern function components that take children should accept them as spread arguments, like this.

95const Row = (...children) => {
96    return {
97        tag: 'div',
98        attrs: {
99            style: {
100                width: '100%',
101                display: 'flex',
102                flexDirection: 'row',
103                justifyContent: 'space-around',
104                alignItems: 'flex-start',
105            },
106        },
107        children: children,
108    }
109}
110
111const Column = (...children) => {
112    return {
113        tag: 'div',
114        attrs: {
115            style: {
116                width: '100%',
117                display: 'flex',
118                flexDirection: 'column',
119                justifyContent: 'space-around',
120                alignItems: 'flex-start',
121            },
122        },
123        children: children,
124    }
125}
126

A general component to center things vertically and horizontally, by using flexboxes centering and taking up all available space.

129const Center = ({
130    horizontal =  true,
131    vertical = true,
132} = {}, ...children) => {
133    return {
134        tag: 'div',
135        attrs: {
136            style: {
137                width: '100%',
138                height: '100%',
139                display: 'flex',
140                flexDirection: 'row',
141                justifyContent: horizontal ? 'center' : 'flex-start',
142                alignItems: vertical ? 'center' : 'flex-start',
143            },
144        },
145        children: children,
146    }
147}
148

Slide component, that takes up the whole page each time.

150
151const Slide = (...children) => {
152    return {
153        tag: 'section',
154        attrs: {
155            class: 'slide',
156            style: {
157                height: '100vh',
158                width: '100vw',
159                boxSizing: 'border-box',
160                display: 'flex',
161                flexDirection: 'column',
162                alignItems: 'center',
163                justifyContent: 'center',
164                overflow: 'hidden',
165                padding: '5vw',
166            },
167        },
168        children: children,
169    }
170}
171

Slide deck component, that controls the entire presentation.

173class Deck extends StyledComponent {
174

We start the presentation at slide 0, and keep track of the list of slides. Every time we advance or rewind, we increment/decrement the slide index and only display that particular slide.

178    init(...slides) {
179        this.slideIndex = 0;
180        this.slides = slides;
181
182        this.setSlideIndex = this.setSlideIndex.bind(this);
183        this.handleAdvanceClick = this.handleAdvanceClick.bind(this);
184        this.handleRewindClick = this.handleRewindClick.bind(this);
185        this.handleKeydown = this.handleKeydown.bind(this);
186

In order to also advance/rewind slides when the user presses left/right arrow keys, we listen for the keydown DOM event.

189        document.addEventListener('keydown', this.handleKeydown);
190    }
191
192    remove() {
193        document.removeEventListener('keydown', this.handleKeydown);
194    }
195
196    setSlideIndex(idx) {
197        this.slideIndex = Math.max(
198            Math.min(
199                this.slides.length - 1,
200                idx
201            ), 0);
202        this.render();
203    }
204
205    advance() {
206        this.setSlideIndex(this.slideIndex + 1);
207    }
208
209    rewind() {
210        this.setSlideIndex(this.slideIndex - 1);
211    }
212
213    handleAdvanceClick(evt) {
214        this.advance();
215    }
216
217    handleRewindClick(evt) {
218        this.rewind();
219    }
220

When the user presses a key, we advance or rewind if it's either of the left/right arrow keys.

223    handleKeydown(evt) {
224        switch (evt.key) {
225            case 'ArrowLeft':
226                this.rewind();
227                break;
228            case 'ArrowRight':
229                this.advance();
230                break;
231        }
232    }
233
234    styles() {
235        return {
236            '.navButton': {
237                'position': 'fixed',
238                'top': 0,
239                'bottom': 0,
240                'background': 'rgba(0, 0, 0, .1)',
241                'color': '#fff',
242                'font-size': '100px',
243                'transition': 'opacity .3s',
244                'transition-delay': '.6s',
245                'width': '10vw',
246                'border-radius': 0,
247                'border': 0,
248                'cursor': 'pointer',
249                'opacity': 0,
250                'outline': 'none',
251                '&:hover': {
252                    'opacity': '1',
253                    'transition-delay': '0s',
254                },
255                '&:active': {
256                    'background': 'rgba(0, 0, 0, .2)',
257                },
258            },
259            '.advanceButton': {
260                'right': 0,
261            },
262            '.rewindButton': {
263                'left': 0,
264            },
265            '.indicators': {
266                'position': 'fixed',
267                'bottom': '2vh',
268                'left': '50vw',
269                'transform': 'translateX(-50%)',
270                'display': 'flex',
271                'opacity': '.7',
272            },
273            '.indicatorDot': {
274                'width': '12px',
275                'height': '12px',
276                'border-radius': '6px',
277                'background': '#eee',
278                'margin': '0 6px',
279                'box-shadow': '0 2px 4px rgba(0, 0, 0, .6)',
280                'transition': 'transform .1s',
281                'cursor': 'pointer',
282                '&.active': {
283                    'background': '#f8f8f8',
284                    'transform': 'scale(1.3)',
285                    'margin': '0 8px',
286                },
287                '&:hover': {
288                    'box-shadow': '0 2px 8px rgba(0, 0, 0, .8)',
289                },
290            },
291        }
292    }
293
294    compose() {
295        return jdom`<main>
296            ${this.slides[this.slideIndex]}
297            <button class="navButton advanceButton" onclick="${this.handleAdvanceClick}">${'>'}</button>
298            <button class="navButton rewindButton" onclick="${this.handleRewindClick}">${'<'}</button>
299            <div class="indicators">
300                ${this.slides.map((s, idx) => {

We could make these place indicator dots individual components, but because they're so simple, they work better as simple functions embedded in another component, like this. Even so, we can still attach event listeners, like this one for example, that brings the user directly to the clicked slide.

305                    return jdom`<div class="indicatorDot ${idx === this.slideIndex ? 'active' : ''}"
306                        onclick="${() => this.setSlideIndex(idx)}"></div>`;
307                })}
308            </div>
309        </main>`;
310    }
311
312}
313

App wrapper around the slide deck, that defines the content.

315class App extends Component {
316
317    init() {

Because the slide deck is one big functional component, we can define our presentation as nested function calls that construct a tree of components to be rendered by the Deck.

321        this.deck = new Deck(
322            Slide(
323                Title('Torus Slide Deck Demo'),
324                Subtitle('This is about this demo!')
325            ),
326            Slide(
327                Title('Building with Torus'),
328                Paragraph('This entire presentation is composed of Torus function components. As you can see, it\'s very easy to compose together components written in this way.'),
329                Paragraph('This ability to create simple units of UI that can be used together and within each other is one of the strengths of following React\'s lead in reusable, composable component-based UI frameworks.')
330            ),
331            Slide(

This pattern of nesting functional components' children as ES2015 spread arguments makes nested functional component code quite readable.

335                Title('Split slide'),
336                Paragraph('This is a split slide, with a more complex layout.'),
337                Row(
338                    Column(
339                        Paragraph('Inspirations for Torus'),
340                        NumberedList(
341                            Paragraph('React'),
342                            Paragraph('Backbone'),
343                            Paragraph('Preact'),
344                            Paragraph('lit-html')
345                        )
346                    ),
347                    Center(
348                        {
349                            horizontal: true,
350                        },
351                        Image({
352                            src: 'https://www.ocf.berkeley.edu/~linuslee/pic.jpg',
353                            alt: 'A picture of me',
354                        })
355                    )
356                )
357            )
358        );
359    }
360
361    compose() {
362        return this.deck.node;
363    }
364
365}
366

Create an instance of the app and mount it to the page DOM.

368const app = new App();
369document.body.appendChild(app.node);
370document.body.style.margin = '0';
371