Remote Control An MDX Deck Presentation
MDX Deck is a great way to create presentation slides however I find it useful to be able to advance the slides when I have stepped away from my computer. I looked to see if this was a solved problem, however I did not find anything. So I then went about looking into how I could do that.
MDX Deck API
First I had to determine if MDX Deck even provided a way to programmatically move the slides and if so where.
My first attempt involved trying to use the Head
component in @mdx-deck/components
however it did not provide access to the
Fortunately the Custom Provider looked like the place to go.
import React from 'react'
// custom provider with content that appears on every slide
const Provider = props => (
<div>
{props.children}
<div
css={{
position: 'fixed',
right: 0,
bottom: 0,
margin: 16,
}}
>
Put your name here
</div>
</div>
)
export default {
Provider,
}
// https://codesandbox.io/s/github/jxnblk/mdx-deck/tree/master/examples/provider
The most important part is that the props passed into the provider include the next
, previous
, and goto
functions I required to control the slides.
First I added a remote control component that will do the work
import React from 'react'
export default ({ next, previous, goto }) => {
return null
}
And then I added the control into the provider
import React from 'react'
import RemoteControl from './remote-control.js'
const Provider = props => (
<div>
{props.children}
<RemoteControl
next={props.next}
previous={props.previous}
goto={props.goto} />
</div>
)
export default {
Provider,
}
But then how to let the remote control component know when to call next or previous? That's where WebSockets come in. WebSockets allow the web page to receive messages from a server. So it's time to setup a server that supports WebSockets. In this case I will be using Hapi and Nes.
In the setup we will need a route to call to invoke the next command and support the WebSocket subscription.
const Hapi = require('@hapi/hapi')
const Nes = require('@hapi/nes')
const init = async () => {
const server = Hapi.server({
port: 8080,
})
await server.register(Nes)
server.subscription('/slides/{id}')
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return 'Hello World!!!'
},
})
server.route({
method: 'GET',
path: '/slide/{deck}/next',
config: {
handler: (r, h) => {
r.server.publish(`/slides/${r.params.deck}`, {
action: 'next',
})
return { result: 'SENT' }
},
},
})
await server.start()
console.log('Server running on %s', server.info.uri)
}
process.on('unhandledRejection', err => {
console.log(err)
process.exit(1)
})
init()
This creates a Hapi web server with the Nes plugin installed, the subscriptions on the /slides/{id}
endpoint and a route /slide/{deck}/next
that when hit calls the subscription passing a message with the action of next.
With that setup it's back to the deck to connect to the server to get the messages and control the slides.
In order to do this I will be using a React Hook from @brightleaf/react-hooks as it has a useNes
hook included.
import React from 'react'
import { useNes } from '@brightleaf/react-hooks/lib/use-nes'
export default ({ next, previous, goto }) => {
const { message, error, connecting, connected, client } = useNes(
'wss://url-goes-here', false
)
const handler = function(update, flags) {
if(update && update.action === 'next') {
next()
}
if(update && update.action === 'previous') {
previous()
}
if(update && update.action === 'goto') {
goto(update.slide)
}
}
client.subscribe('/slides/slide-deck-1', handler)
return null
}
This uses a react hook that returns the nes client that then subscribes to the broadcast endpoint and when a message is received the handler checks the action property of the message and performs the action requested.
So if you go to https://4yeq0.sse.codesandbox.io/slide/slide-deck-1/next
you will see the deck advance a slide.
You can see the slide deck here and the server here
Now with the mechanics sorted for moving the slides, it's time to put a UI together that can be used.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useGet } from "@brightleaf/react-hooks/lib/use-get";
import {
Button, Column, Columns, Container, Control, Hero,
HeroBody, Section, Title, SubTitle
} from "@brightleaf/elements";
import { Form, TextInput } from "react-form-elements";
import "./styles.css";
function App() {
const [deck, setDeck] = useState("slide-deck-1");
const { getUrl: getNext } = useGet(
`https://4yeq0.sse.codesandbox.io/slide/${deck}/next`
);
const { getUrl: getPrevious } = useGet(
`https://4yeq0.sse.codesandbox.io/slide/${deck}/previous`
);
return (
<>
<Hero isBold isInfo>
<HeroBody>
<Title>Remote Control</Title>
<SubTitle>Press the buttons to see some magic happen!</SubTitle>
</HeroBody>
</Hero>
<Section className="App">
<Container>
<Columns>
<Column isHalf>
<Button
isPrimary
isLarge
isFullWidth
className="is-tall"
onClick={e => {
e.preventDefault();
getPrevious(
`https://4yeq0.sse.codesandbox.io/slide/${deck}/previous`
);
}}
>
<< Previous
</Button>
</Column>
<Column isHalf>
<Button
isPrimary
isLarge
isFullWidth
className="is-tall"
onClick={e => {
e.preventDefault();
console.log("click next");
getNext(
`https://4yeq0.sse.codesandbox.io/slide/${deck}/next`
);
}}
>
Next >>
</Button>
</Column>
</Columns>
<hr />
<Columns>
<Column isFull>
<Form
name="slidepicker"
onSubmit={data => {
setDeck(data.slides);
}}
>
<TextInput
className="field control"
labelClassName="label is-large"
inputClassName="input is-large"
name="slides"
initialValue=""
label="Slide Deck"
/>
<Control>
<Button isInfo>Connect Slide Deck</Button>
</Control>
</Form>
</Column>
</Columns>
</Container>
</Section>
</>
);
}
And in action
In addition to the codesandbox links in the article, the code that inspired the post can be found on GitHub.
Top comments (0)