I've been a fan of Reveal.js for many years. Reveal.js is a web-based presentation framework that makes it (mostly) easy to create slides with just basic HTML.
I don't mind Powerpoint at all, and it's incredibly powerful, but when I'm presenting on web topics (which is, usually, 99% of the time), I don't like the experience of "alt-tabbing" from a running Powerpoint to code or browser tabs.
Reveal.js helps me avoid that.
Using Reveal (I'm getting tired of typing the dot jay ess) is relatively simple. You add a script tag. You add two link tags for CSS (one for the core CSS and one for a theme), and then start writing HTML. The section
tag is used for each slide. Here's an example of how easy it is, from their docs:
<html>
<head>
<link rel="stylesheet" href="dist/reveal.css">
<link rel="stylesheet" href="dist/theme/white.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>Slide 1</section>
<section>Slide 2</section>
</div>
</div>
<script src="dist/reveal.js"></script>
<script>
Reveal.initialize();
</script>
</body>
</html>
In the snippet above, you can see the two section
tags representing each slide. There's quite a bit to it, and if you want to see more, check out their demo, but for today, I was curious if I could simplify the creation of Reveal presentations with web components. Here's what I came up with.
First, I'll share the HTML I wanted to use. I actually wrote this first and then began building the web component so I could see it update itself.
<reveal-preso theme="beige">
<section>Slide 1b</section>
<section>Slide 2</section>
<section>Slide 3</section>
<section data-background-color="aquamarine">
<h2>🍦</h2>
</section>
<section data-background-image="https://placekitten.com/800/800">
<h2>Image</h2>
</section>
</reveal-preso>
My HTML makes use of the web component, reveal-preso
, and supports a theme
attribute. Inside the component are a set of section
tags for the slides. (As an FYI, I could have also built a reveal-slide
child tag, and I will do that in my next post!) I don't have any script or link tags at all.
Now, let's look at the component definition.
class RevealPreso extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.theme = 'black';
if(this.hasAttribute('theme')) {
this.theme = this.getAttribute('theme');
}
let currentHTML = this.innerHTML;
this.innerHTML = `
<div class="reveal">
<div class="slides">
${currentHTML}
</div>
</div>
`;
// load reveal.js
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/reveal.js';
document.head.appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type='text/css';
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/reveal.min.css';
document.head.appendChild(link);
const theme = document.createElement('link');
theme.rel = 'stylesheet';
theme.type='text/css';
theme.href = `https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/theme/${this.theme}.min.css`;
document.head.appendChild(theme);
script.addEventListener('load', () => {
Reveal.initialize();
});
}
}
if(!customElements.get('reveal-preso')) customElements.define('reveal-preso', RevealPreso);
As you can see, this is relatively simple. My connectedCallback
rewrites the inner HTML such that it's wrapped with the divs
that Reveal expects. I then dynamically load all three resources - my JavaScript and both CSS resources.
I've got an event listener on the script such that when it's been loaded, I can initialize the presentation.
From what I could see (via Googling and Stack Overflow), there are no onload
for CSS scripts. I saw a few suggested workarounds, but I decided to keep it simple and just worry about the JavaScript resource. I accept that may be problematic.
One more issue - my code checks for the theme argument one time only. If you were to change it via JavaScript later, it would not pick up on that.
(If I may be so bold as to suggest a best practice here, I'd probably suggest you always have code to recognize changes to attributes or clearly document what's not going to work.) Here's how it looks:
Personally, I think this is pretty cool! But notice that it expects the entire web view. That's the default for Reveal and probably how most people would use it, but I really wanted to see if it was possible to embed Reveal along with other web content. Here's how I did that.
First, I checked the docs. I had only ever used Reveal as a full web view presentation, and I didn't know if the framework itself supported it. Turns out, it absolutely has an embedded mode. There's also support for multiple embedded presentations.
This requires two things - a slight change to how you initialize the Reveal object and supplying specific dimensions of the container holding your presentation.
I began by modifying my HTML a bit:
<h2>Preso</h2>
<p>
Here is my preso. It brings all the boys to the yard.
</p>
<reveal-preso height="700px" theme="dracula">
<section>Slide 1b</section>
<section>
<h2>Plan</h2>
<ul>
<li class="fragment">Phase One - Collect Underpants</li>
<li class="fragment">Phase Two - ?</li>
<li class="fragment">Phase Three - Profit</li>
</ul>
</section>
<section>Slide 3</section>
<section data-background-color="aquamarine">
<h2>🍦</h2>
</section>
<section data-background-image="https://placekitten.com/800/800">
<h2>Image</h2>
</section>
</reveal-preso>
<footer>
End of page.
</footer>
I've got some basic crap on top and bottom, and in the reveal-preso
tag, I've added a height
. My component is going to support both height
and width
, but it will default both. Now, let's look at the updated component:
class RevealPreso extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
console.log('connected callback called');
// defaults
this.width = '100%';
this.height = '500px';
this.theme = 'black';
if(this.hasAttribute('width')) {
this.width = this.getAttribute('width');
}
if(this.hasAttribute('height')) {
this.height = this.getAttribute('height');
}
if(this.hasAttribute('theme')) {
this.theme = this.getAttribute('theme');
}
let currentHTML = this.innerHTML;
this.innerHTML = `
<div class="reveal" style="width: ${this.width}; height: ${this.height}">
<div class="slides">
${currentHTML}
</div>
</div>
`;
// load reveal.js
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/reveal.js';
document.head.appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type='text/css';
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/reveal.min.css';
document.head.appendChild(link);
const theme = document.createElement('link');
theme.rel = 'stylesheet';
theme.type='text/css';
theme.href = `https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.5.0/theme/${this.theme}.min.css`;
document.head.appendChild(theme);
script.addEventListener('load', () => {
Reveal(this.querySelector( '.reveal' ), {
embedded: true,
keyboardCondition: 'focused' // only react to keys when focused
}).initialize();
});
}
}
if(!customElements.get('reveal-preso')) customElements.define('reveal-preso', RevealPreso);
I've updated the code to look for height
and width
now and default both. When I rewrite the inner HTML content, I include those values. Lastly, I changed how I initialize the presentation. You can see this in action here:
There are still things I'd tweak here. I'd definitely add support for changing the height and width. Reveal itself also supports recognizing changes to its size.
One note - I did do a quick test with 2 presentations on the page, and it worked, but it only shows one theme which I believe there is no workaround as Reveal expects only one theme CSS.
It may be possible, but I'm not sure.
As I said earlier, I want to iterate on this one more time. I want to create a child tag, reveal-slide
(or perhaps something shorter) and look into importing the tag from another JavaScript resource.
I also want to support dynamically changing the height and width. I'll work on this tomorrow or later this month. As always, let me know what you think!