Slide deck demo annotated source
Back to indexSlides 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