diff --git a/content/docs/portals.md b/content/docs/portals.md index e8b486f539f..debc2a8fc47 100644 --- a/content/docs/portals.md +++ b/content/docs/portals.md @@ -46,7 +46,7 @@ A typical use case for portals is when a parent component has an `overflow: hidd > > It is important to remember, when working with portals, you'll need to make sure to follow the proper accessibility guidelines. -[Try it on CodePen.](https://codepen.io/gaearon/pen/yzMaBd) +[Try it on CodeSandbox.](https://codesandbox.io/embed/github/CompuIves/reactjs.org/tree/codesandbox/examples/portals-1?codemirror=1) ## Event Bubbling Through Portals @@ -139,6 +139,6 @@ function Child() { ReactDOM.render(, appRoot); ``` -[Try it on CodePen.](https://codepen.io/gaearon/pen/jGBWpE) +[Try it on CodeSandbox.](https://codesandbox.io/embed/github/CompuIves/reactjs.org/tree/codesandbox/examples/portals-2?codemirror=1) Catching an event bubbling up from a portal in a parent component allows the development of more flexible abstractions that are not inherently reliant on portals. For example, if you render a `` component, the parent can capture its events regardless of whether it's implemented using portals. diff --git a/content/docs/state-and-lifecycle.md b/content/docs/state-and-lifecycle.md index 3074727cf8f..ee286eee6c6 100644 --- a/content/docs/state-and-lifecycle.md +++ b/content/docs/state-and-lifecycle.md @@ -13,50 +13,13 @@ So far we have only learned one way to update the UI. We call `ReactDOM.render()` to change the rendered output: -```js{8-11} -function tick() { - const element = ( -
-

Hello, world!

-

It is {new Date().toLocaleTimeString()}.

-
- ); - ReactDOM.render( - element, - document.getElementById('root') - ); -} - -setInterval(tick, 1000); -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/gwoJZk?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-1.js{11}?editorsize=70&forcerefresh=1) In this section, we will learn how to make the `Clock` component truly reusable and encapsulated. It will set up its own timer and update itself every second. We can start by encapsulating how the clock looks: -```js{3-6,12} -function Clock(props) { - return ( -
-

Hello, world!

-

It is {props.date.toLocaleTimeString()}.

-
- ); -} - -function tick() { - ReactDOM.render( - , - document.getElementById('root') - ); -} - -setInterval(tick, 1000); -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/dpdoYR?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-2.js{6,7,8,9,14}?editorsize=70&forcerefresh=1) However, it misses a crucial requirement: the fact that the `Clock` sets up a timer and updates the UI every second should be an implementation detail of the `Clock`. @@ -89,20 +52,7 @@ You can convert a functional component like `Clock` to a class in five steps: 5. Delete the remaining empty function declaration. -```js -class Clock extends React.Component { - render() { - return ( -
-

Hello, world!

-

It is {this.props.date.toLocaleTimeString()}.

-
- ); - } -} -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/zKRGpo?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-3.js?editorsize=70&forcerefresh=1) `Clock` is now defined as a class rather than a function. @@ -171,30 +121,7 @@ We will later add the timer code back to the component itself. The result looks like this: -```js{2-5,11,18} -class Clock extends React.Component { - constructor(props) { - super(props); - this.state = {date: new Date()}; - } - - render() { - return ( -
-

Hello, world!

-

It is {this.state.date.toLocaleTimeString()}.

-
- ); - } -} - -ReactDOM.render( - , - document.getElementById('root') -); -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/KgQpJd?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-4.js{5,6,7,8,14,20}?editorsize=70) Next, we'll make the `Clock` set up its own timer and update itself every second. @@ -265,47 +192,7 @@ Finally, we will implement a method called `tick()` that the `Clock` component w It will use `this.setState()` to schedule updates to the component local state: -```js{18-22} -class Clock extends React.Component { - constructor(props) { - super(props); - this.state = {date: new Date()}; - } - - componentDidMount() { - this.timerID = setInterval( - () => this.tick(), - 1000 - ); - } - - componentWillUnmount() { - clearInterval(this.timerID); - } - - tick() { - this.setState({ - date: new Date() - }); - } - - render() { - return ( -
-

Hello, world!

-

It is {this.state.date.toLocaleTimeString()}.

-
- ); - } -} - -ReactDOM.render( - , - document.getElementById('root') -); -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/amqdNA?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-5.js{18,19,20,21,22}?editorsize=70) Now the clock ticks every second. @@ -434,13 +321,7 @@ This also works for user-defined components: The `FormattedDate` component would receive the `date` in its props and wouldn't know whether it came from the `Clock`'s state, from the `Clock`'s props, or was typed by hand: -```js -function FormattedDate(props) { - return

It is {props.date.toLocaleTimeString()}.

; -} -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/zKRqNB?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-6.js{4,5,6}?editorsize=70) This is commonly called a "top-down" or "unidirectional" data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components "below" them in the tree. @@ -448,24 +329,7 @@ If you imagine a component tree as a waterfall of props, each component's state To show that all components are truly isolated, we can create an `App` component that renders three ``s: -```js{4-6} -function App() { - return ( -
- - - -
- ); -} - -ReactDOM.render( - , - document.getElementById('root') -); -``` - -[Try it on CodePen.](http://codepen.io/gaearon/pen/vXdGmd?editors=0010) +[Run example.](source:examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/index.js{9,10,11}?editorsize=70) Each `Clock` sets up its own timer and updates independently. diff --git a/content/docs/uncontrolled-components.md b/content/docs/uncontrolled-components.md index f1bf1cdc9b0..f97abf71f52 100644 --- a/content/docs/uncontrolled-components.md +++ b/content/docs/uncontrolled-components.md @@ -10,33 +10,7 @@ To write an uncontrolled component, instead of writing an event handler for ever For example, this code accepts a single name in an uncontrolled component: -```javascript{8,17} -class NameForm extends React.Component { - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(event) { - alert('A name was submitted: ' + this.input.value); - event.preventDefault(); - } - - render() { - return ( -
- - -
- ); - } -} -``` - -[Try it on CodePen.](https://codepen.io/gaearon/pen/WooRWa?editors=0010) +[Run the example.](source:examples/single-file-examples/src/uncontrolled-components.js{11,20}?editorsize=70) Since an uncontrolled component keeps the source of truth in the DOM, it is sometimes easier to integrate React and non-React code when using uncontrolled components. It can also be slightly less code if you want to be quick and dirty. Otherwise, you should usually use controlled components. diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 00000000000..d85dfa0ca84 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,21 @@ +!public + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/portals-1/package.json b/examples/portals-1/package.json new file mode 100644 index 00000000000..18011a37f31 --- /dev/null +++ b/examples/portals-1/package.json @@ -0,0 +1,16 @@ +{ + "name": "portals-1", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-scripts": "1.0.14" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} \ No newline at end of file diff --git a/examples/portals-1/public/favicon.ico b/examples/portals-1/public/favicon.ico new file mode 100644 index 00000000000..a11777cc471 Binary files /dev/null and b/examples/portals-1/public/favicon.ico differ diff --git a/examples/portals-1/public/index.html b/examples/portals-1/public/index.html new file mode 100644 index 00000000000..05dea56e77c --- /dev/null +++ b/examples/portals-1/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + React App + + + + +
+ + + + + \ No newline at end of file diff --git a/examples/portals-1/src/App.js b/examples/portals-1/src/App.js new file mode 100644 index 00000000000..feacfc8eb51 --- /dev/null +++ b/examples/portals-1/src/App.js @@ -0,0 +1,50 @@ +import React from 'react'; + +import Modal from './Modal'; + +// The Modal component is a normal React component, so we can +// render it wherever we like without needing to know that it's +// implemented with portals. +export default class App extends React.Component { + constructor(props) { + super(props); + this.state = {showModal: false}; + + this.handleShow = this.handleShow.bind(this); + this.handleHide = this.handleHide.bind(this); + } + + handleShow() { + this.setState({showModal: true}); + } + + handleHide() { + this.setState({showModal: false}); + } + + render() { + // Show a Modal on click. + // (In a real app, don't forget to use ARIA attributes + // for accessibility!) + const modal = this.state.showModal ? ( + +
+
+ With a portal, we can render content into a different part of the + DOM, as if it were any other React child. +
+ This is being rendered inside the #modal-container div. + +
+
+ ) : null; + + return ( +
+ This div has overflow: hidden. + + {modal} +
+ ); + } +} diff --git a/examples/portals-1/src/Modal.js b/examples/portals-1/src/Modal.js new file mode 100644 index 00000000000..47a8219c11e --- /dev/null +++ b/examples/portals-1/src/Modal.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +const modalRoot = document.getElementById('modal-root'); + +// Let's create a Modal component that is an abstraction around +// the portal API. +export default class Modal extends React.Component { + constructor(props) { + super(props); + // Create a div that we'll render the modal into. Because each + // Modal component has its own element, we can render multiple + // modal components into the modal container. + this.el = document.createElement('div'); + } + + componentDidMount() { + // Append the element into the DOM on mount. We'll render + // into the modal container element (see the HTML tab). + modalRoot.appendChild(this.el); + } + + componentWillUnmount() { + // Remove the element from the DOM when we unmount + modalRoot.removeChild(this.el); + } + + render() { + // Use a portal to render the children into the element + return ReactDOM.createPortal( + // Any valid React child: JSX, strings, arrays, etc. + this.props.children, + // A DOM element + this.el, + ); + } +} diff --git a/examples/portals-1/src/index.css b/examples/portals-1/src/index.css new file mode 100644 index 00000000000..0dd55982d93 --- /dev/null +++ b/examples/portals-1/src/index.css @@ -0,0 +1,23 @@ +#modal-root { + position: relative; + z-index: 999; +} + +.app { + height: 10em; + width: 10em; + background: lightblue; + overflow: hidden; +} + +.modal { + background-color: rgba(0,0,0,0.5); + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/examples/portals-1/src/index.js b/examples/portals-1/src/index.js new file mode 100644 index 00000000000..1e8f33b6674 --- /dev/null +++ b/examples/portals-1/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App'; +import './index.css'; + +// These two containers are siblings in the DOM +const appRoot = document.getElementById('app-root'); + +ReactDOM.render(, appRoot); diff --git a/examples/portals-2/package.json b/examples/portals-2/package.json new file mode 100644 index 00000000000..21a00ea9457 --- /dev/null +++ b/examples/portals-2/package.json @@ -0,0 +1,16 @@ +{ + "name": "portals-2", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-scripts": "1.0.14" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} \ No newline at end of file diff --git a/examples/portals-2/public/favicon.ico b/examples/portals-2/public/favicon.ico new file mode 100644 index 00000000000..a11777cc471 Binary files /dev/null and b/examples/portals-2/public/favicon.ico differ diff --git a/examples/portals-2/public/index.html b/examples/portals-2/public/index.html new file mode 100644 index 00000000000..05dea56e77c --- /dev/null +++ b/examples/portals-2/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + React App + + + + +
+ + + + + \ No newline at end of file diff --git a/examples/portals-2/src/Child.js b/examples/portals-2/src/Child.js new file mode 100644 index 00000000000..8bc29662a70 --- /dev/null +++ b/examples/portals-2/src/Child.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function Child() { + // The click event on this button will bubble up to parent, + // because there is no 'onClick' attribute defined + return ( +
+ +
+ ); +} diff --git a/examples/portals-2/src/Modal.js b/examples/portals-2/src/Modal.js new file mode 100644 index 00000000000..d3ff28ecc80 --- /dev/null +++ b/examples/portals-2/src/Modal.js @@ -0,0 +1,23 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +const modalRoot = document.getElementById('modal-root'); + +export default class Modal extends React.Component { + constructor(props) { + super(props); + this.el = document.createElement('div'); + } + + componentDidMount() { + modalRoot.appendChild(this.el); + } + + componentWillUnmount() { + modalRoot.removeChild(this.el); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.el); + } +} diff --git a/examples/portals-2/src/Parent.js b/examples/portals-2/src/Parent.js new file mode 100644 index 00000000000..aa170ff82b3 --- /dev/null +++ b/examples/portals-2/src/Parent.js @@ -0,0 +1,36 @@ +import React from 'react'; + +import Modal from './Modal'; +import Child from './Child'; + +export default class Parent extends React.Component { + constructor(props) { + super(props); + this.state = {clicks: 0}; + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + // This will fire when the button in Child is clicked, + // updating Parent's state, even though button + // is not direct descendant in the DOM. + this.setState(prevState => ({ + clicks: prevState.clicks + 1, + })); + } + + render() { + return ( +
+

Number of clicks: {this.state.clicks}

+

+ Open up the browser DevTools to observe that the button is not a child + of the div with the onClick handler. +

+ + + +
+ ); + } +} diff --git a/examples/portals-2/src/index.css b/examples/portals-2/src/index.css new file mode 100644 index 00000000000..3b946139738 --- /dev/null +++ b/examples/portals-2/src/index.css @@ -0,0 +1,23 @@ +#app-root { + height: 10em; + width: 10em; + background: lightblue; + overflow: hidden; +} + +#modal-root { + position: relative; + z-index: 999; +} + +.modal { + background-color: rgba(0,0,0,0.5); + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/examples/portals-2/src/index.js b/examples/portals-2/src/index.js new file mode 100644 index 00000000000..108dab93bfa --- /dev/null +++ b/examples/portals-2/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Parent from './Parent'; +import './index.css'; + +// These two containers are siblings in the DOM +const appRoot = document.getElementById('app-root'); + +ReactDOM.render(, appRoot); diff --git a/examples/single-file-examples/package.json b/examples/single-file-examples/package.json new file mode 100644 index 00000000000..34270a83fee --- /dev/null +++ b/examples/single-file-examples/package.json @@ -0,0 +1,16 @@ +{ + "name": "react-doc-examples", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-scripts": "1.0.14" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} \ No newline at end of file diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-1.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-1.js new file mode 100644 index 00000000000..c621159768b --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-1.js @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +function tick() { + const element = ( +
+

Hello, world!

+

It is {new Date().toLocaleTimeString()}.

+
+ ); + ReactDOM.render(element, document.getElementById('root')); +} + +setInterval(tick, 1000); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-2.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-2.js new file mode 100644 index 00000000000..87c1efb4d4a --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-2.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +function Clock(props) { + return ( +
+

Hello, world!

+

It is {props.date.toLocaleTimeString()}.

+
+ ); +} + +function tick() { + ReactDOM.render(, document.getElementById('root')); +} + +setInterval(tick, 1000); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-3.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-3.js new file mode 100644 index 00000000000..678e8732016 --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-3.js @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +class Clock extends React.Component { + render() { + return ( +
+

Hello, world!

+

It is {this.props.date.toLocaleTimeString()}.

+
+ ); + } +} + +function tick() { + ReactDOM.render(, document.getElementById('root')); +} + +setInterval(tick, 1000); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-4.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-4.js new file mode 100644 index 00000000000..08969d164fd --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-4.js @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +class Clock extends React.Component { + constructor(props) { + super(props); + this.state = {date: new Date()}; + } + + render() { + return ( +
+

Hello, world!

+

It is {this.state.date.toLocaleTimeString()}.

+
+ ); + } +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-5.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-5.js new file mode 100644 index 00000000000..f9cc82af11e --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-5.js @@ -0,0 +1,34 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +class Clock extends React.Component { + constructor(props) { + super(props); + this.state = {date: new Date()}; + } + + componentDidMount() { + this.timerID = setInterval(() => this.tick(), 1000); + } + + componentWillUnmount() { + clearInterval(this.timerID); + } + + tick() { + this.setState({ + date: new Date(), + }); + } + + render() { + return ( +
+

Hello, world!

+

It is {this.state.date.toLocaleTimeString()}.

+
+ ); + } +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-6.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-6.js new file mode 100644 index 00000000000..9867b7d7d77 --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-6.js @@ -0,0 +1,38 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +function FormattedDate(props) { + return

It is {props.date.toLocaleTimeString()}.

; +} + +class Clock extends React.Component { + constructor(props) { + super(props); + this.state = {date: new Date()}; + } + + componentDidMount() { + this.timerID = setInterval(() => this.tick(), 1000); + } + + componentWillUnmount() { + clearInterval(this.timerID); + } + + tick() { + this.setState({ + date: new Date(), + }); + } + + render() { + return ( +
+

Hello, world!

+ +
+ ); + } +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/Clock.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/Clock.js new file mode 100644 index 00000000000..0e6a0668356 --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/Clock.js @@ -0,0 +1,35 @@ +import React from 'react'; + +function FormattedDate(props) { + return

It is {props.date.toLocaleTimeString()}.

; +} + +export default class Clock extends React.Component { + constructor(props) { + super(props); + this.state = {date: new Date()}; + } + + componentDidMount() { + this.timerID = setInterval(() => this.tick(), 1000); + } + + componentWillUnmount() { + clearInterval(this.timerID); + } + + tick() { + this.setState({ + date: new Date(), + }); + } + + render() { + return ( +
+

Hello, world!

+ +
+ ); + } +} diff --git a/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/index.js b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/index.js new file mode 100644 index 00000000000..37c8fe930b3 --- /dev/null +++ b/examples/single-file-examples/src/state-and-lifecycle/state-and-lifecycle-7/index.js @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Clock from './Clock'; + +function App() { + return ( +
+ + + +
+ ); +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/single-file-examples/src/uncontrolled-components.js b/examples/single-file-examples/src/uncontrolled-components.js new file mode 100644 index 00000000000..2e11c3fae4a --- /dev/null +++ b/examples/single-file-examples/src/uncontrolled-components.js @@ -0,0 +1,28 @@ +import React from 'react'; +import {render} from 'react-dom'; + +class NameForm extends React.Component { + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(event) { + alert('A name was submitted: ' + this.input.value); + event.preventDefault(); + } + + render() { + return ( +
+ + +
+ ); + } +} + +render(, document.getElementById('root')); diff --git a/gatsby-config.js b/gatsby-config.js index 029197a38a3..5d9cd33cb84 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -54,6 +54,7 @@ module.exports = { maxWidth: 840, }, }, + 'gatsby-remark-codesandbox', 'gatsby-remark-autolink-headers', 'gatsby-remark-use-jsx', { diff --git a/package.json b/package.json index 1d214c9f022..45c5388431a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "gatsby-plugin-sharp": "^1.6.2", "gatsby-plugin-twitter": "^1.0.10", "gatsby-remark-autolink-headers": "^1.4.4", + "gatsby-remark-codesandbox": "^1.2.23", "gatsby-remark-copy-linked-files": "^1.5.2", "gatsby-remark-images": "^1.5.11", "gatsby-remark-prismjs": "^1.2.1", diff --git a/src/components/MarkdownPage/MarkdownPage.js b/src/components/MarkdownPage/MarkdownPage.js index c71a90f4dd7..7bbe86483d8 100644 --- a/src/components/MarkdownPage/MarkdownPage.js +++ b/src/components/MarkdownPage/MarkdownPage.js @@ -19,108 +19,136 @@ import toCommaSeparatedList from 'utils/toCommaSeparatedList'; import {sharedStyles} from 'theme'; import createOgUrl from 'utils/createOgUrl'; -const MarkdownPage = ({ - authors, - createLink, - date, - enableScrollSync, - ogDescription, - location, - markdownRemark, - sectionList, - titlePostfix = '', -}) => { - const hasAuthors = authors.length > 0; - const titlePrefix = markdownRemark.frontmatter.title || ''; - - return ( - - -
- -
- - - - {(date || hasAuthors) && ( -
- {date}{' '} - {hasAuthors && ( - - by{' '} - {toCommaSeparatedList(authors, author => ( - - {author.frontmatter.name} - - ))} - - )} -
- )} +class MarkdownPage extends React.Component { + componentDidMount() { + const codeSandboxLinks = document.querySelectorAll( + "a[href^='https://codesandbox.io']", + ); -
-
+ codeSandboxLinks.forEach(link => { + link.onclick = e => { + const codeBlock = link.previousSibling; + + if (codeBlock && codeBlock.className === 'gatsby-highlight') { + const [child] = codeBlock.children; + if (child && child.tagName !== 'IFRAME') { + e.preventDefault(); + + link.textContent = 'Open in CodeSandbox.'; + codeBlock.innerHTML = ``; + } + } + }; + }); + } - {markdownRemark.fields.path && ( -
- - Edit this page - + render() { + const { + authors, + createLink, + date, + enableScrollSync, + ogDescription, + location, + markdownRemark, + sectionList, + titlePostfix = '', + } = this.props; + + const hasAuthors = authors.length > 0; + const titlePrefix = markdownRemark.frontmatter.title || ''; + + return ( + + +
+ +
+ + + + {(date || hasAuthors) && ( +
+ {date}{' '} + {hasAuthors && ( + + by{' '} + {toCommaSeparatedList(authors, author => ( + + {author.frontmatter.name} + + ))} + + )}
)} + +
+
+ + {markdownRemark.fields.path && ( + + )} +
+ + +
+
- - -
-
-
- -
- - {/* TODO Read prev/next from index map, not this way */} - {(markdownRemark.frontmatter.next || markdownRemark.frontmatter.prev) && ( - - )} - - ); -}; +
+
+ + {/* TODO Read prev/next from index map, not this way */} + {(markdownRemark.frontmatter.next || + markdownRemark.frontmatter.prev) && ( + + )} +
+ ); + } +} MarkdownPage.defaultProps = { authors: [], diff --git a/yarn.lock b/yarn.lock index a86fa6c6bd7..cf05322b173 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2332,14 +2332,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - cross-env@^3.1.1: version "3.2.4" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-3.2.4.tgz#9e0585f277864ed421ce756f81a980ff0d698aba" @@ -3942,6 +3934,15 @@ gatsby-remark-autolink-headers@^1.4.4: mdast-util-to-string "^1.0.2" unist-util-visit "^1.1.1" +gatsby-remark-codesandbox@^1.2.23: + version "1.2.23" + resolved "https://registry.yarnpkg.com/gatsby-remark-codesandbox/-/gatsby-remark-codesandbox-1.2.23.tgz#7da743b0160bf6ef0a8518f9fab4de5881e98741" + dependencies: + babel-runtime "^6.26.0" + git-branch "^1.0.0" + git-username "^0.5.0" + unist-util-visit "^1.1.1" + gatsby-remark-copy-linked-files@^1.5.2: version "1.5.7" resolved "https://registry.yarnpkg.com/gatsby-remark-copy-linked-files/-/gatsby-remark-copy-linked-files-1.5.7.tgz#59817877cb2978a6a72df62f2353033f0f8a6e35" @@ -4217,6 +4218,16 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +git-branch@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/git-branch/-/git-branch-1.0.0.tgz#64cc7dd75da2d81a9d4679087c1f8b56e6bd2d3d" + +git-username@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/git-username/-/git-username-0.5.0.tgz#38c561dac9cbf334097a31bd9a38af0cb40a3123" + dependencies: + remote-origin-url "^0.4.0" + github-slugger@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.1.3.tgz#314a6e759a18c2b0cc5760d512ccbab549c549a7" @@ -4438,7 +4449,7 @@ graphql-skip-limit@^1.0.5: dependencies: babel-runtime "^6.26.0" -graphql@^0.10.3, graphql@^0.10.5: +graphql@0.10.5, graphql@^0.10.3, graphql@^0.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.10.5.tgz#c9be17ca2bdfdbd134077ffd9bbaa48b8becd298" dependencies: @@ -4910,7 +4921,7 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" -ini@^1.3.4, ini@~1.3.0: +ini@^1.3.3, ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -6801,6 +6812,12 @@ parse-filepath@^1.0.1: map-cache "^0.2.0" path-root "^0.1.1" +parse-git-config@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/parse-git-config/-/parse-git-config-0.2.0.tgz#272833fdd15fea146fb75d336d236b963b6ff706" + dependencies: + ini "^1.3.3" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -7755,16 +7772,7 @@ react-deep-force-update@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-2.0.1.tgz#4f7f6c12c3e7de42f345992a3c518236fa1ecad3" -react-dom@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" - dependencies: - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - -react-dom@^16.0.0: +react-dom@16.0.0, react-dom@^15.6.0, react-dom@^16.0.0: version "16.0.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0.tgz#9cc3079c3dcd70d4c6e01b84aab2a7e34c303f58" dependencies: @@ -7827,17 +7835,7 @@ react-side-effect@^1.1.0: exenv "^1.2.1" shallowequal "^1.0.1" -react@^15.6.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" - dependencies: - create-react-class "^15.6.0" - fbjs "^0.8.9" - loose-envify "^1.1.0" - object-assign "^4.1.0" - prop-types "^15.5.10" - -react@^16.0.0: +react@16.0.0, react@^15.6.0, react@^16.0.0: version "16.0.0" resolved "https://registry.yarnpkg.com/react/-/react-16.0.0.tgz#ce7df8f1941b036f02b2cca9dbd0cb1f0e855e2d" dependencies: @@ -8130,6 +8128,12 @@ remarkable@^1.7.1: argparse "~0.1.15" autolinker "~0.15.0" +remote-origin-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/remote-origin-url/-/remote-origin-url-0.4.0.tgz#4d3e2902f34e2d37d1c263d87710b77eb4086a30" + dependencies: + parse-git-config "^0.2.0" + remote-redux-devtools@^0.5.7: version "0.5.12" resolved "https://registry.yarnpkg.com/remote-redux-devtools/-/remote-redux-devtools-0.5.12.tgz#42cb95dfa9e54c1d9671317c5e7bba41e68caec2"