doczjs / docz

✍ It has never been so easy to document your things!
https://docz.site
MIT License
23.66k stars 1.46k forks source link

Sharing scope between embeded code and <Playground> #881

Closed sonhanguyen closed 5 years ago

sonhanguyen commented 5 years ago

Sometimes you might want to create some ad-hoc example code and immediately use it on the playground.

## Example
'''jsx
const Component = () => 'test'
'''

<Playground>
  <Component />
</Playground>

The alternative is having the code imported from another module but then it would not be the same because you can't show it in the docs.

danielkcz commented 5 years ago

Related #894

sonhanguyen commented 5 years ago

Hi @FredyC, I've found a way to do it in user land by overriding the theme. Basically you can wrap the theme with your theme which uses <ComponentsProvider> to provide custom pre and playground component and inside the custom theme you can hook those two up since you have access to both's sources. I was only haft way through it last night, will share some code when I'm done.

wangpin34 commented 5 years ago

@FredyC +1. I got the same requirement during developing the UI lib for my team. @sonhanguyen You idea seems great, but I do not really get what we can do in <ComponentsProvider>. Can't wait for your update. thanks.

sonhanguyen commented 5 years ago

This is what I came up with @wangpin34

// theme.jsx

import * as React from 'react'
import { ComponentsProvider, useComponents } from 'docz'
import { PlaygroundProps } from 'docz-theme-default/dist/components/ui/Playground'
import { transform } from 'buble'

const createPlayground = (Default, scope) => (props: PlaygroundProps) =>{
  scope = { ...props.scope, ...scope }
  return <Default { ...props } scope={scope} /> 
}

const createPre = (Default, Blockquote, scope) => props => {
  const { props: childProps, type } = props.children
  if (!childProps) { return null }

  const {
    className, // language-<ext>
    mdxType, originalType, // code
    parentName, // pre
    children,
    hidden, ['run-with']: toRun, // added
    metastring,
    ...meta
  } = childProps

  if (toRun) {
    const jsx = '$h'
    const props = { [jsx]: React.createElement, ...scope, ...meta }
    const VAR_NAME = /^[\$_\w]+$/i

    const code = `
      var {${Object.keys(props).filter(key => key.match(VAR_NAME))}} = props;
      ${transform(children, { jsx }).code}
    `
    try {
      const func = new Function('exports', 'props', code)
      func(scope, props)
    } catch (e) { return <>
        <Blockquote>{e.message}</Blockquote>
        <Default { ...props }>{code.trim()}</Default>
      </>
    }
  }

  return hidden ? null : <Default { ...props } />
}

const usePerInstance = <T extends any>(factory: () => T) => React.useMemo(factory, [])

const ModuleScopeProvider = props => {
  const components = useComponents()
  const { playground, blockquote, pre } = components
  const scope = usePerInstance(Object)

  return <ComponentsProvider components={{
      ...components,
      playground: usePerInstance(() => createPlayground(playground, scope)),
      pre: usePerInstance(() => createPre(pre, blockquote, scope))
    }}>
    {props.children}
  </ComponentsProvider>
}

const enhance = Theme => props => <Theme {...props}>
  <ModuleScopeProvider children={props.children} />
</Theme>

export default enhance(require('docz-theme-default').default)

And this is how you use it

# doc.mdx

` ``jsx hidden run-with text=Hello
exports.Test = () => <span>{text}</span>
` ``
 Currently there is a caveat which is that the block which mutates the scope need to be before <Playground>

<Playground>
  <Test />
</Playground>

I tagged @FredyC since he is interested in the issue, not sure how it solves you guys' problem tbh. However I'm curious too so if you come up with something let me know