Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support state #66

Open
markdalgleish opened this issue Apr 26, 2019 · 15 comments · May be fixed by #262
Open

Support state #66

markdalgleish opened this issue Apr 26, 2019 · 15 comments · May be fixed by #262
Labels
enhancement New feature or request

Comments

@markdalgleish
Copy link
Member

markdalgleish commented Apr 26, 2019

It'd be great if users could make their designs stateful.

My thinking is that we could do this in a code-oriented way by allowing usage of React hooks like this:

const [ firstName, setFirstName ] = useState('');
const [ lastName, setLastName ] = useState('');

<TextField label="First name" value={name} onChange=(event => setFirstName(event.target.value)} />
<TextField label="Last name" value={name} onChange=(event => setLastName(event.target.value)} />

For this to work, we'd need to treat the last n number of JSX elements as an implicitly returned fragment, i.e. behind the scenes we'd turn it into a component that looks like this:

() => {
  const [ firstName, setFirstName ] = useState('');
  const [ lastName, setLastName ] = useState('');

  return (
    <React.Fragment>
      <TextField label="First name" value={name} onChange=(event => setFirstName(event.target.value)} />
      <TextField label="Last name" value={name} onChange=(event => setName(event.target.value)} />
    </React.Fragment>
  );
};
@markdalgleish markdalgleish added the enhancement New feature or request label Apr 26, 2019
@kvendrik
Copy link

kvendrik commented Apr 26, 2019

As an alternative to consider last week we did some thinking on if we would be able to let people set hooks within the JSX somehow using say a custom component. Not sure if this should be something that is directly implemented into Playroom itself but thought the idea was interesting. I do wonder though if and how limiting this would be.

<PlayroomState defaultState={false}>
  {(toggled, setToggled) => {
    <Button onClick={() => setToggled(true)}>Open</Button>
    <Modal
      open={toggled}
      onClose={() => setToggled(false)}
    >
      Modal content
    </Modal>
  }}
</PlayroomState>

Or to make it even easier we have also thought about creating helpers that have default state values to allow non-devs to understand and rapidly prototype using state even quicker:

<ToggleState>
  {(isToggled, toggle) => {
    <Button onClick={toggle}>Open</Button>
    <Modal
      open={isToggled}
      onClose={toggle}
    >
      Modal content
    </Modal>
  }}
</ToggleState>

As we plan on trying to use Playroom as a way for our designers to prototype setting state using the least amount of JS complexity would be the ideal scenario for us. This doesn't have to be something that is necessarily directly integrated into Playroom though. I can for example imagine that within Playroom we just want some way of setting state and then devs can create their own wrappers around that depending on their target audience.

@danrosenthal
Copy link

danrosenthal commented Apr 26, 2019

Here's a quick-n-dirty pull request that creates some wrapped functions and uses context magic to enable no-config state that gets us pretty close to what I consider to be the ideal prototyping API from a designer's perspective.

Shopify/polaris#1372

I see an approach like this as following the 80/20 rule. Any components that require state, or any components that require lots of config, would follow this approach. That gets you to about 80% of your rapid prototyping needs. Having hooks available gets you the other 20% of the way there.

I see this as the ideal compromise for designer prototyping productivity.

The API this gives us for building a stateful Popover with Polaris then is really simple in Playroom:

<Play.ToggleState>
  <Play.Popover
    sectioned
    activator={<Play.ToggleButton>Toggle</Play.ToggleButton>}
  >
    <p>Some popover content</p>
  </Play.Popover>
</Play.ToggleState>

@markdalgleish
Copy link
Member Author

That's interesting. Anything component-based is inherently supported by Playroom already. I see the potential here for people to create component-oriented state management libraries designed for prototyping. If you come up with something along these lines, I might see if I can find a way to talk about it in the readme.

@hipstersmoothie
Copy link

You can already kind of do this in your components file:

import { Checkbox as CheckboxComponent } from './Choice';
export const Checkbox = props => {
  const [checked, set] = React.useState(props.checked);

  return (
    <CheckboxComponent
      {...props}
      checked={checked}
      onChange={() => set(!checked)}
    />
  );
};

Having a way to do this without creating the wrapper though would be interesting

@armand1m
Copy link

armand1m commented Feb 27, 2020

I just got a very simple solution:

In my components file, I export this one line component:

export const FunctionWrapper = ({ children }) => children()

so it allows me to do the following on playroom:

<FunctionWrapper>
  {() => {
    const [state, setState] = React.useState(0);

    return (
      <>
        <p>{state}</p>
        <button onClick={() => setState(prevState => prevState + 1)}>
          Increase
        </button>
      </>
    );
  }}
</FunctionWrapper>

@ezhikov
Copy link

ezhikov commented Mar 12, 2020

Hi. Since code now transpiled through babel, it's possible to alter it, so it would be component itself. I coded some solution, that takes user code, wraps it into React.createElement(function () {}) and adds return to jsx code.

const [value, setValue] = React.useState('');

<input value={value} onChange={event => setValue(event.target.value)}/>

is transformed to

React.createElement(function() {
  const [value, setValue] = React.useState('');

  return <input value={value} onChange={event => setValue(event.target.value)}/>
})

I see interesting side-effect of this solution - you don't need React.Fragment anymore.

Right now solution is fastcoded as PoC, so there is few broken tests. If you interested, I can continue working on it.

@adrienharnay
Copy link
Contributor

This would be a very neat improvement! Any way I can help on this? 😄

@pascalduez
Copy link
Contributor

I just went with an IIFE:

{(() => {
  const [possible, setPossible] = React.useState(false);

  return (
    <p onClick={() => setPossible(true)}>
      Can I use state in playroom: {possible ? "yep" : "dunno"}
    </p>
  );
})()}

@markdalgleish
Copy link
Member Author

markdalgleish commented Oct 22, 2020

@pascalduez Your approach is definitely simpler, but only recently became possible because the internal component that wraps your JSX used to be a class component, so Hooks were not supported.

(Just in case anyone was wondering why this hadn't been tried earlier)

@pascalduez
Copy link
Contributor

but only recently became possible because the internal component that wraps your JSX used to be a class component, so Hooks were not supported

That's it! I remember an unsuccessful try some time ago, so I was happily surprised yesterday :)

@moatorres
Copy link

Another workaround: use React's default prop alongside with children to create a <State> wrapper.

playroom/components.js

export const State = (props) => {
  const result = React.useState(props.default)

  return props.children(result)
}

And use it like so:

<State>
  {([state, setState]) => {
    return (
      <button onClick={() => setState(!state)}>
        {state ? "Click me" : "Hey!"}
      </button>
    );
  }}
</State>

@jesstelford
Copy link
Contributor

jesstelford commented Sep 14, 2022

I've been looking into how we can have Playroom support state in a nicer way, and have rediscovered this issue.

Mark's initial example is interesting, and I started looking at ways we could use Babel to compile this.

I discovered @ezhikov's attempt which seemed like a good start: ezhikov@9af06f0

But I quickly realised that the Babel JSX parser has a serious limitation: It throws an error when there are adjacent JSX elements not wrapped in a fragment.

Screen Shot 2022-09-14 at 5 20 03 pm

After a few false starts I realised: Mark's example is just MDX! Try pasting the below example into the MDX playground, then select the mdast tab:

const [ firstName, setFirstName ] = useState('');
const [ lastName, setLastName ] = useState('');

<TextField label="First name" value={name} onChange={event => setFirstName(event.target.value)} />
<TextField label="Last name" value={name} onChange={event => setLastName(event.target.value)} />

The const lines are "paragraphs" in MDX terms, but otherwise what we've got here is a parsable, valid MDX snippet where the last two child nodes of the root AST are "mdxJsxFlowElement"s.

The AST also comes with positional information.

So, using this, we can parse the code with mdast , iterate over the children in reverse order, until we've discovered all the JSX elements at the end of the snippet (ie; the two <TextField>s in this example. Then we look at the position data in the AST for the first & last of those elements. That indicates where we need to insert return (<React.Fragment> and </React.Fragment> into the code, then we can wrap the entire thing in an IIFE, and finally pass it to babel to compile to executable code!

A touch complicated, and it assumes mdast can run in the browser without significant overhead to parse things on demand, but... If it works, it'd be pretty epic!

What do you think @markdalgleish @michaeltaranto? I'm happy to spend a bit of time working on a PR to give this a crack if you think it's worth it?

@michaeltaranto
Copy link
Contributor

Hey Jess 👋 , this was a bit of a walk back through time reading all the comments, moving from class to function components etc. 😄

We took a different approach in Braid and ultimately ended up building the custom scope feature for Playroom to support it. The scope feature allows variables to be provided into the frame context, which can be used for variables or theme tokens, but equally functions for state interactions.

We have found this to be pretty flexible to our needs, and remember considering moving it to Playroom:

// playroom.config.js
const stateScope = require('playroom/scopeTemplates/state');

module.exports = {
  scope: stateScope,
  ...
}

Here is a working example in the Braid Playroom.

What are your thoughts on this approach?

@jesstelford
Copy link
Contributor

Loving the custom scope feature! We're currently using it to expose all of React's exports so we don't have to write React. everywhere, and also to expose our Design System's custom hooks - works fantastically!

The custom state functions are interesting, but they still require some funky syntax to get working, and don't appear to support arbitrary JS like @pascalduez's IIFE solution does.

For context, we're auto-generating Playroom links by loading example code from our documentation site. That example code might be POR (Plain Ol' React 😝) which uses state, or has JS logic baked in before rendering. Right now, we're automatically inserting an IIFE which makes everything work, but adds ugly syntax to the code editor, and is hard to remember/get right when writing an example from scratch.

Ideally, your example could be rewritten like:

const [screen, setScreen] = useState("Home");

{getState("screen") === "Home" && (
  <Card>
    <Stack space="large">
      <Heading level="3">Home</Heading>
      <Actions>
        <Button onClick={() => setScreen("dashboard")}>Sign in</Button>
      </Actions>
    </Stack>
  </Card>
)}

{getState("screen") === "dashboard" && (
  <Card>
    <Stack space="large">
      <Heading level="3">👋 Welcome!</Heading>
      <Placeholder height={100} />
      <Actions>
        <Button onClick={() => setScreen("Home")}>Sign out</Button>
      </Actions>
    </Stack>
  </Card>
)}

@jesstelford
Copy link
Contributor

On reflection with @gwyneplaine, we realised that what we really want is just a way to hook into the Babel processing step; We can perform any IIFE wrapping or MDX parsing there without having to bake our strong opinions into Playroom.

To that end, I've opened a PR adding a new processCode config: #262

Additional Code Transformations

A hook into the internal processing of code is available via the processCode option, which is a path to a file that exports a function that receives the code as entered into the editor, and returns the new code to be rendered.

What do you think of this approach?

If we end up building something more complex, I'm happy to post it here for future folks to discover and add to their own projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.