|
| 1 | +--- |
| 2 | +title: "Custom components" |
| 3 | +date: "2018-01-27" |
| 4 | +draft: false |
| 5 | +author: Jay Gatsby |
| 6 | +tags: |
| 7 | + - remark |
| 8 | + - React |
| 9 | + - components |
| 10 | +--- |
| 11 | + |
| 12 | +What if you want custom UI interactions embedded in your Markdown? |
| 13 | + |
| 14 | +By using `rehype-react` with the `htmlAst` field, you can write custom React components and then reference them from your Markdown files. |
| 15 | + |
| 16 | +## Writing a component |
| 17 | + |
| 18 | +Write the component the way you normally would. For example, here's a simple "Counter" component: |
| 19 | + |
| 20 | +```js |
| 21 | +import React from "react" |
| 22 | + |
| 23 | +const counterStyle = { |
| 24 | + /* styles skipped for brevity */ |
| 25 | +} |
| 26 | + |
| 27 | +export default class Counter extends React.Component { |
| 28 | + static defaultProps = { |
| 29 | + initialvalue: 0, |
| 30 | + } |
| 31 | + |
| 32 | + state = { |
| 33 | + value: Number(this.props.initialvalue), |
| 34 | + } |
| 35 | + |
| 36 | + handleIncrement = () => { |
| 37 | + this.setState(state => { |
| 38 | + return { |
| 39 | + value: state.value + 1, |
| 40 | + } |
| 41 | + }) |
| 42 | + } |
| 43 | + |
| 44 | + handleDecrement = () => { |
| 45 | + this.setState(state => { |
| 46 | + return { |
| 47 | + value: state.value - 1, |
| 48 | + } |
| 49 | + }) |
| 50 | + } |
| 51 | + |
| 52 | + render() { |
| 53 | + return ( |
| 54 | + <span style={counterStyle}> |
| 55 | + <strong style={{ flex: `1 1` }}> |
| 56 | + {this.state.value} |
| 57 | + </strong> |
| 58 | + <button onClick={this.handleDecrement}>-1</button> |
| 59 | + <button onClick={this.handleIncrement}>+1</button> |
| 60 | + </span> |
| 61 | + ) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +``` |
| 66 | + |
| 67 | +## Enabling the component |
| 68 | + |
| 69 | +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: |
| 70 | + |
| 71 | +1. Install `rehype-react` as a dependency |
| 72 | + |
| 73 | + ```bash |
| 74 | + # If you use Yarn |
| 75 | + yarn add rehype-react |
| 76 | + |
| 77 | + # If you use npm |
| 78 | + npm install --save rehype-react |
| 79 | + ``` |
| 80 | + |
| 81 | +2. Import `rehype-react` and whichever components you wish to use |
| 82 | + |
| 83 | + ```js |
| 84 | + import rehypeReact from "rehype-react" |
| 85 | + import Counter from "../components/Counter" |
| 86 | + ``` |
| 87 | + |
| 88 | +3. Create a render function with references to your custom components |
| 89 | + |
| 90 | + ```js |
| 91 | + const renderAst = new rehypeReact({ |
| 92 | + createElement: React.createElement, |
| 93 | + components: { "interactive-counter": Counter }, |
| 94 | + }).Compiler |
| 95 | + ``` |
| 96 | + |
| 97 | + I prefer to use hyphenated names to make it clear that it's a custom component. |
| 98 | +
|
| 99 | +4. Render your content using `htmlAst` instead of `html` |
| 100 | +
|
| 101 | + 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: |
| 102 | + |
| 103 | + ```js |
| 104 | + <div dangerouslySetInnerHTML={{ __html: post.html }} /> |
| 105 | + ``` |
| 106 | + |
| 107 | + with this: |
| 108 | + |
| 109 | + ```js |
| 110 | + {renderAst(post.htmlAst)} |
| 111 | + ``` |
| 112 | + |
| 113 | +5. Change `html` to `htmlAst` in your `pageQuery` |
| 114 | + |
| 115 | + ```graphql |
| 116 | + # ... |
| 117 | + markdownRemark(fields: { slug: { eq: $slug } }) { |
| 118 | + htmlAst # previously `html` |
| 119 | +
|
| 120 | + # other fields... |
| 121 | + } |
| 122 | + # ... |
| 123 | + ``` |
| 124 | +
|
| 125 | +## Using the component |
| 126 | +
|
| 127 | +Now, you can directly use your custom component within your Markdown files! For instance, if you include the tag: |
| 128 | +
|
| 129 | +```html |
| 130 | +<interactive-counter></interactive-counter> |
| 131 | +``` |
| 132 | +
|
| 133 | +You'll end up with an interactive component: |
| 134 | +
|
| 135 | +<interactive-counter></interactive-counter> |
| 136 | +
|
| 137 | +In addition, you can also pass attributes, which can be used as props in your component: |
| 138 | +
|
| 139 | +```html |
| 140 | +<interactive-counter initialvalue="10"></interactive-counter> |
| 141 | +``` |
| 142 | +
|
| 143 | +<interactive-counter initialvalue="10"></interactive-counter> |
| 144 | +
|
| 145 | +## Caveats |
| 146 | +
|
| 147 | +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. |
| 148 | +
|
| 149 | +### Always add closing tags |
| 150 | +
|
| 151 | +JSX allows us to write self-closing tags, such as `<my-component />`. 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: |
| 152 | +
|
| 153 | +```html |
| 154 | +<my-component></my-component> |
| 155 | +``` |
| 156 | +
|
| 157 | +### Attribute names are always lowercased |
| 158 | +
|
| 159 | +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. |
| 160 | +
|
| 161 | +> 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`. |
| 162 | +
|
| 163 | +### Attributes are always strings |
| 164 | +
|
| 165 | +Any prop that gets its value from an attribute will always receive a string value. Your component is responsible for properly deserializing passed values. |
| 166 | +
|
| 167 | +- Numbers are always passed as strings, even if the attribute is written without quotes: `<my-component value=37></my-component>` will still receive the string `"37"` as its value instead of the number `37`. |
| 168 | +- React lets you pass a prop without a value, and will interpret it to mean `true`; for example, if you write `<MyComponent isEnabled />` 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. |
| 169 | +- 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. `<my-thing objectvalue='{"foo": "bar"}'></my-thing>`). |
| 170 | +
|
| 171 | +> Notice in the `Counter` example how the initial `value` has been cast using the `Number()` function. |
| 172 | +
|
| 173 | +## Possibilities |
| 174 | +
|
| 175 | +Custom components embedded in Markdown enables many features that weren't possible before; here are some ideas, starting simple and getting complex: |
| 176 | +
|
| 177 | +- Write a live countdown clock for an event such as Christmas, the Super Bowl, or someone's birthday. Suggested markup: `<countdown-clock> 2019-01-02T05:00:00.000Z </countdown-clock>` |
| 178 | +- Write a component that displays as a link with an informative hovercard. For example, you might want to write `<hover-card subject="ostrich"> ostriches </hover-card>` to show a link that lets you hover to get information on ostriches. |
| 179 | +- 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. |
| 180 | +- 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. |
| 181 | +- Write a component that wraps a [GFM table](./hello-world-kitchen-sink/#tables) and displays the data from the table in an interactive graph. |
0 commit comments