From 9ba98a1d2d1efe1fc3a8b843a78d91188655d21b Mon Sep 17 00:00:00 2001 From: Ryan Kennedy Date: Sat, 27 Jan 2018 11:26:27 -0500 Subject: [PATCH] [examples/using-remark]: Custom React components in Markdown files --- examples/using-remark/package.json | 1 + .../using-remark/src/components/Counter.js | 47 +++++ .../2018-01-27---custom-components/index.md | 181 ++++++++++++++++++ .../src/templates/template-blog-post.js | 11 +- 4 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 examples/using-remark/src/components/Counter.js create mode 100644 examples/using-remark/src/pages/2018-01-27---custom-components/index.md diff --git a/examples/using-remark/package.json b/examples/using-remark/package.json index 1ff6a593acc07..e5192e900c942 100644 --- a/examples/using-remark/package.json +++ b/examples/using-remark/package.json @@ -29,6 +29,7 @@ "lodash-id": "^0.14.0", "lodash-webpack-plugin": "^0.11.2", "react-typography": "^0.15.10", + "rehype-react": "^3.0.2", "slash": "^1.0.0", "typeface-space-mono": "^0.0.22", "typeface-spectral": "^0.0.29", diff --git a/examples/using-remark/src/components/Counter.js b/examples/using-remark/src/components/Counter.js new file mode 100644 index 0000000000000..ea51a2202c5a3 --- /dev/null +++ b/examples/using-remark/src/components/Counter.js @@ -0,0 +1,47 @@ +import React from "react" + +const counterStyle = { + display: `flex`, + flexFlow: `row nowrap`, + alignItems: `center`, + padding: `0.2em 0.4em`, + borderRadius: `2px`, + backgroundColor: `rgba(0, 0, 0, 0.2)`, + boxShadow: `inset 2px 1.5px 4px rgba(0, 0, 0, 0.2)`, +} + +export default class Counter extends React.Component { + static defaultProps = { + initialvalue: 0, + } + + state = { + value: Number(this.props.initialvalue), + } + + handleIncrement = () => { + this.setState(state => { + return { + value: state.value + 1, + } + }) + } + + handleDecrement = () => { + this.setState(state => { + return { + value: state.value - 1, + } + }) + } + + render() { + return ( + + {this.state.value} + + + + ) + } +} diff --git a/examples/using-remark/src/pages/2018-01-27---custom-components/index.md b/examples/using-remark/src/pages/2018-01-27---custom-components/index.md new file mode 100644 index 0000000000000..09e47f20b3f51 --- /dev/null +++ b/examples/using-remark/src/pages/2018-01-27---custom-components/index.md @@ -0,0 +1,181 @@ +--- +title: "Custom components" +date: "2018-01-27" +draft: false +author: Jay Gatsby +tags: + - remark + - React + - components +--- + +What if you want custom UI interactions embedded in your Markdown? + +By using `rehype-react` with the `htmlAst` field, you can write custom React components and then reference them from your Markdown files. + +## Writing a component + +Write the component the way you normally would. For example, here's a simple "Counter" component: + +```js +import React from "react" + +const counterStyle = { + /* styles skipped for brevity */ +} + +export default class Counter extends React.Component { + static defaultProps = { + initialvalue: 0, + } + + state = { + value: Number(this.props.initialvalue), + } + + handleIncrement = () => { + this.setState(state => { + return { + value: state.value + 1, + } + }) + } + + handleDecrement = () => { + this.setState(state => { + return { + value: state.value - 1, + } + }) + } + + render() { + return ( + + + {this.state.value} + + + + + ) + } +} + +``` + +## Enabling the component + +In order to display this component within a Markdown file, you'll need to add a reference to the component in the template that renders your Markdown content. There are five parts to this: + +1. Install `rehype-react` as a dependency + + ```bash + # If you use Yarn + yarn add rehype-react + + # If you use npm + npm install --save rehype-react + ``` + +2. Import `rehype-react` and whichever components you wish to use + + ```js + import rehypeReact from "rehype-react" + import Counter from "../components/Counter" + ``` + +3. Create a render function with references to your custom components + + ```js + const renderAst = new rehypeReact({ + createElement: React.createElement, + components: { "interactive-counter": Counter }, + }).Compiler + ``` + + I prefer to use hyphenated names to make it clear that it's a custom component. + +4. Render your content using `htmlAst` instead of `html` + + This will look different depending on how you were previously referring to the post object retrieved from GraphQL, but in general you'll want to replace this: + + ```js +
+ ``` + + with this: + + ```js + {renderAst(post.htmlAst)} + ``` + +5. Change `html` to `htmlAst` in your `pageQuery` + + ```graphql + # ... + markdownRemark(fields: { slug: { eq: $slug } }) { + htmlAst # previously `html` + + # other fields... + } + # ... + ``` + +## Using the component + +Now, you can directly use your custom component within your Markdown files! For instance, if you include the tag: + +```html + +``` + +You'll end up with an interactive component: + + + +In addition, you can also pass attributes, which can be used as props in your component: + +```html + +``` + + + +## Caveats + +Although it looks like we're now using React components in our Markdown files, that's not _entirely_ true: we're adding custom HTML elements which are then replaced by React components. This means there are a few things to watch out for. + +### Always add closing tags + +JSX allows us to write self-closing tags, such as ``. However, the HTML parser would incorrectly interpret this as an opening tag, and be unable to find a closing tag. For this reason, tags written in Markdown files always need to be explicitly closed, even when empty: + +```html + +``` + +### Attribute names are always lowercased + +HTML attribute names are not case-sensitive. `gatsby-transformer-remark` handles this by lowercasing all attribute names; this means that any props on an exposed component must also be all-lowercase. + +> The prop in the `Counter` example above was named `initialvalue` rather than `initialValue` for exactly this reason; if we tried to access `this.props.initialValue` we'd have found it to be `undefined`. + +### Attributes are always strings + +Any prop that gets its value from an attribute will always receive a string value. Your component is responsible for properly deserializing passed values. + +- Numbers are always passed as strings, even if the attribute is written without quotes: `` will still receive the string `"37"` as its value instead of the number `37`. +- React lets you pass a prop without a value, and will interpret it to mean `true`; for example, if you write `` then the props would be `{ isEnabled: true }`. However, in your Markdown, an attribute without a value will not be interpreted as `true`; instead, it will be passed as the empty string `""`. Similarly, passing `somevalue=true` and `othervalue=false` will result in the string values `"true"` and `"false"`, respectively. +- You can pass object values if you use `JSON.parse()` in your component to get the value out; just remember to enclose the value in single quotes to ensure it is parsed correctly (e.g. ``). + +> Notice in the `Counter` example how the initial `value` has been cast using the `Number()` function. + +## Possibilities + +Custom components embedded in Markdown enables many features that weren't possible before; here are some ideas, starting simple and getting complex: + +- Write a live countdown clock for an event such as Christmas, the Super Bowl, or someone's birthday. Suggested markup: ` 2019-01-02T05:00:00.000Z ` +- Write a component that displays as a link with an informative hovercard. For example, you might want to write ` ostriches ` to show a link that lets you hover to get information on ostriches. +- If your Gatsby site is for vacation photos, you might write a component that allows you to show a carousel of pictures, and perhaps a map that shows where each photo was taken. +- Write a component that lets you add live code demos in your Markdown, using [component-playground](https://formidable.com/open-source/component-playground/) or something similar. +- Write a component that wraps a [GFM table](./hello-world-kitchen-sink/#tables) and displays the data from the table in an interactive graph. diff --git a/examples/using-remark/src/templates/template-blog-post.js b/examples/using-remark/src/templates/template-blog-post.js index c2b4d3528682f..b0bdd88f11b39 100644 --- a/examples/using-remark/src/templates/template-blog-post.js +++ b/examples/using-remark/src/templates/template-blog-post.js @@ -1,13 +1,20 @@ import React from "react" import Link from "gatsby-link" import Img from "gatsby-image" +import rehypeReact from "rehype-react" import styles from "../styles" import { rhythm, scale } from "../utils/typography" import presets from "../utils/presets" +import Counter from "../components/Counter" import "katex/dist/katex.min.css" +const renderAst = new rehypeReact({ + createElement: React.createElement, + components: { "interactive-counter": Counter }, +}).Compiler + class BlogPostRoute extends React.Component { render() { const post = this.props.data.markdownRemark @@ -69,7 +76,7 @@ class BlogPostRoute extends React.Component { className="toc" /> -
+ {renderAst(post.htmlAst)}