loading...

Remote Control An MDX Deck Presentation

kevnz profile image Kevin Isom ・4 min read

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()

View On CodeSandbox

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`
                  );
                }}
              >
                &lt;&lt; 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 &gt;&gt;
              </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.

Posted on by:

kevnz profile

Kevin Isom

@kevnz

I write code, I like to talk about code, I have opinions about code.

Discussion

pic
Editor guide