A quick preview of what we'll build in this tutorial.
Before starting, you can check the live example.
Installation
We'll create a new Gatsby project from this starter template that has build-in support for an MDX blog and the prism-react-renderer module.
gatsby new gatsby-starter-blog-mdx <https://github.com/hagnerd/gatsby-starter-blog-mdx>
Understand the basics of MDX
The entry point for MDX is the MDXProvider
component that handles internally the mapping of components to MDX. Also, it has a very important prop.
The components
prop is an object that allows you to override the default component for each HTML element (here is a list for them) or even provide your own as shortcodes.
The Gatsby template uses the MDXProvider
inside the wrapRootElement
browser API.
The wrapRootElement browser API is useful to set up any Provider components that will wrap your application.
Below you see the wrap-root-element.js
file that sets up the MDXProvider
and overrides the pre
element with a custom Code
component.
import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Code } from "./src/components/code"
import { preToCodeBlock } from "mdx-utils"
const components = {
pre: (preProps) => {
const props = preToCodeBlock(preProps)
if (props) {
return <Code {...props} />
} else {
return <pre {...preProps} />
}
},
}
export const wrapRootElement = ({ element }) => (
<MDXProvider components={components}>{element}</MDXProvider>
)
Then, our wrapper is added to both gatsby-browser
and gatsby-ssr.js
files to render the root element of the Gatsby app.
import { wrapRootElement as wrap } from "./wrap-root-element"
export const wrapRootElement = wrap
Adjust the custom code component
The custom Code
component lives in the src/components/code.js
file and utilizes the prism-react-renderer
. The prism-react-renderer
is the perfect way to render some extra UI with your Prismjs-highlighted code.
The library tokenises code using Prism and provides a small render-props-driven component to quickly render it out into React.
The default code.js
is the following:
import React from "react"
import { render } from "react-dom"
import Highlight, { defaultProps } from "prism-react-renderer"
import { LiveProvider, LiveEditor, LiveError, LivePreview } from "react-live"
export const Code = ({ codeString, language, ...props }) => {
if (props["react-live"]) {
return (
<LiveProvider code={codeString} noInline={true}>
<LiveEditor />
<LiveError />
<LivePreview />
</LiveProvider>
)
} else {
return (
<Highlight {...defaultProps} code={codeString} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
}
The magic happens inside Highlight
component. The pre
element renders the code wrapper and the render-prop functions provide the necessary props for each line and token/word.
The copy to clipboard button will live inside the pre
element.
Create the copy button
The copy button will be placed at the top right corner of the code section.
To achieve that positioning, we'll set the position of the pre
element to relative
and add a little bit of padding.
<pre
className={className}
style={{
...style,
padding: "2rem",
position: "relative",
}}
>
...
</pre>
The Button
component is a simple button element that is placed with position: absolute
:
const Button = (props) => (
<button
style={{
position: "absolute",
top: 0,
right: 0,
border: "none",
boxShadow: "none",
textDecoration: "none",
margin: "8px",
padding: "8px 12px",
background: "#E2E8F022",
color: "white",
borderRadius: "8px",
cursor: "pointer",
color: "#E2E8F0",
fontSize: "14px",
fontFamily: "sans-serif",
lineHeight: "1",
}}
{...props}
/>
)
For a better UX, your users should be informed about the outcome of their actions. So it's a nice extra feature to toggle the button's text once the code is copied.
Then, we have to set a React hook state variable isCopied
.
const [isCopied, setIsCopied] = React.useState(false)
The isCopied
variable gets true when the user clicks the copy button and resets to false after a specific amount of time (eg. 3 seconds).
<Button
onClick={() => {
copyToClipboard(codeString)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 3000)
}}
>
{isCopied ? "🎉 Copied!" : "Copy"}
</Button>
The copyToClipboard
is our core functionality here. I've re-used a function from this article.
const copyToClipboard = (str) => {
const el = document.createElement("textarea")
el.value = str
el.setAttribute("readonly", "")
el.style.position = "absolute"
el.style.left = "-9999px"
document.body.appendChild(el)
el.select()
document.execCommand("copy")
document.body.removeChild(el)
}
The final code component
By now, we have the custom Code
component, the copyToClipboard
function, and the Button
component. Then, the final code component is the following:
export const Code = ({ codeString, children, language, ...props }) => {
const [isCopied, setIsCopied] = React.useState(false)
if (props["react-live"]) {
return (
<LiveProvider code={codeString} noInline={true}>
<LiveEditor />
<LiveError />
<LivePreview />
</LiveProvider>
)
} else {
return (
<Highlight
{...defaultProps}
code={codeString}
language={language}
theme={dracula}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={className}
style={{
...style,
padding: "2rem",
position: "relative",
}}
>
<Button
onClick={() => {
copyToClipboard(codeString)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 3000)
}}
>
{isCopied ? "🎉 Copied!" : "Copy"}
</Button>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })} style={style}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
}
The Confetti party
The code works fine. The copy-to-clipboard functionality is perfect. But we still miss the flair!
There is a known secret in the frontend development community!
Everything is better with a little confetti
It's so useless but we're gonna congratulate our readers with a confetti rain.
To bring this to life, we'll have to install the React dependency react-dom-confetti
.
yarn add react-dom-confetti
The configuration is pretty straightforward. It's just a JSON object with a couple of options:
const config = {
angle: 90,
spread: 360,
startVelocity: 40,
elementCount: 70,
dragFriction: 0.12,
duration: 3000,
stagger: 3,
width: "10px",
height: "10px",
perspective: "500px",
colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"],
}
The next step is to add the Confetti
component. This component explodes to a confetti rain every time the prop active
is true.
Then, we just have to pass the isCopied variable to make it explode in our example. Easy, huh?
<Confetti active={isCopied} config={config} />
To place the Confetti component, we can use the same positioning trick as before. Because we want to fire the explosion in front of the button.
We'll set up a Wrapper
component for the Confetti
and Highlight
components with the CSS attribute position: relative
. Then, we'll wrap the Confetti component with the ConfettiWrapper
which is placed absolutely at the top right corner.
<Wrapper>
<Highlight>...</Highlight>
<ConfettiWrapper>
<Confetti active={isCopied} config={config} />
</ConfettiWrapper>
</Wrapper>
And the code for the two wrappers:
const Wrapper = (props) => <div style={{ position: "relative" }} {...props} />
const ConfettiWrapper = (props) => (
<div style={{ position: "absolute", top: 0, right: 0 }} {...props} />
)
That's all folks
Clone the Github repository and don't forget to show me your creation by tagging me, @d__raptis on Twitter 💪