storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.1k stars 9.25k forks source link

How to use context? #76

Closed davidpelaez closed 8 years ago

davidpelaez commented 8 years ago

Currently many UI libraries like material-ui depend on the context for theming. I wasn't able to figure out a way to setup this. Maybe I should have a global container and use it to wrap all my stories? It doesn't feel 100% clean, but I wanted to check if this if the correct approach.

Thanks for react-storybook, this is amazing! We've wanted this for so long in our team, it's currently changing the way we work with new projects. :)

stewartduffy commented 8 years ago

Hi @davidpelaez we are doing this by setting context on each component. Not ideal, but in the context of a component library which we are building - it makes sense, having each component be self containing so they can be used in isolation.

import React from 'react'
import ThemeManager from 'material-ui/lib/styles/theme-manager'
import myTheme from '../theme/my-theme' //path to your custom theme
import AppBar from 'material-ui/lib/app-bar';

const ExampleComponent = React.createClass({
  propTypes: {
    children: React.PropTypes.any.isRequired
  },

  childContextTypes: {
    muiTheme: React.PropTypes.object
  },

  getChildContext () {
    return {
      muiTheme: ThemeManager.getMuiTheme(myTheme)
    }
  },

  render () {
    return (
      <div>
        <AppBar title="Title" iconClassNameRight="muidocs-icon-navigation-expand-more" />
      </div>
    )
  }
})

export default ExampleComponent
arunoda commented 8 years ago

@davidpelaez, They way you do it the correct way to do it. But, I think we can introduce a new API to make these things a bit easier.

See:

import { storiesOf } from '@kadira/storybook';
import Context from '../context';
import MyComp from '../my_comp';

storiesOf('MyComp').
  .addDecorator(function(getStory) {
    return (<Context>getStory()</Context>)
  })
  .add('default view', () => (
    <MyComp>Something here</MyComp>
  ))

This is just a nice syntax but it will wrap each story with the context. But, I think that's the way it should be anyway.

mcbain commented 8 years ago

The same is needed for material-ui. Currently I wrap every story with a <WithTheme>/<WithTheme> component. +1

davidpelaez commented 8 years ago

@mcbain exactly I came to this because of material-ui. Thanks for pointing out that using the wrapper was correct. It would be nice to either have this is in the docs or have a dedicated helper for it. A slightly closer integration to small details of React like this would be great. I've seen many constants update in the last dates and you are doing an amazing job. Whatever you choose will work great :)

David Peláez Tamayo Designer & Entrepreneur

On Fri, Apr 15, 2016 at 1:47 AM, mcbain notifications@github.com wrote:

The same is needed for material-ui. Currently I wrap every story with a

/ component. +1 — You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/kadirahq/react-storybook/issues/76#issuecomment-210309813
arunoda commented 8 years ago

@mnmtanish could you work on the addDecorator() API when you have some time. Make a comment if you are working on this.

If some else could do it before @mnmtanish that's great too :)

thani-sh commented 8 years ago

@arunoda just wrote a simple implementation here. Let me know if there are any changes. It should work but I haven't tested it with a React demo yet. I'll comment here after testing in a while.

necolas commented 8 years ago

Could you not use something like https://github.com/mattzeunert/react-with-context?

wuzhuzhu commented 8 years ago

I'm tring to use storybook with Material-ui. And the Dropdown menu in material-ui not response for clicking. After reading above I have tried to use new addDecorator API, but import Context from '../context'; cause new Error:

...
ERROR in ./client/configs/context.js
Module not found: Error: Cannot resolve module 'meteor/reactive-dict' in /Users/walter/WebstormProjects/meteor13test/client/configs
 @ ./client/configs/context.js 25:20-51
...

here is content in context.js:

import * as Collections from '/lib/index';
import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router-ssr';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Tracker } from 'meteor/tracker';

export default function () {
    return {
        Meteor,
        FlowRouter,
        Collections,
        LocalState: new ReactiveDict(),
        Tracker,
    };
}
arunoda commented 8 years ago

@wuzhuzhu

Storybook can't read meteor/xxx dependencies since they are some special deps managed by Meteor. Seems like you are using Mantra and you should not import context directly. That may fix your issue.

VirtueMe commented 8 years ago

@wuzhuzhu

I had the same issue with SelectField, the answer is in the material-ui readme.md You need to load the react-tap-event-plugin when storybook loads.

Some components use react-tap-event-plugin to listen for touch events because onClick is not fast enough This dependency is temporary and will eventually go away. Until then, be sure to inject this plugin at the start of your app.

import injectTapEventPlugin from 'react-tap-event-plugin';

// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();
wuzhuzhu commented 8 years ago

Thanks for both of your answers! It's working now.

kjetilge commented 8 years ago

Is it possible get formsy-react to work with storybook / meteor-mantra ? All fields just disappears in storybook.

ffxsam commented 8 years ago

@VirtueMe Where did you wind up putting the injectTapEventPlugin() call? I keep getting errors, because it can only be invoked once. I'm not sure what the best place for it is.

VirtueMe commented 8 years ago

I added it to the .storybook/config.js file.

michaltakac commented 8 years ago

Was trying to use Material UI with storybook today and came up with this solution:

/stories/materialize.js:

import React from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import defaultTheme from '../../../common/app/defaultTheme';

export default function materialize(Component, props) {
  return (
    <MuiThemeProvider muiTheme={defaultTheme}>
      <Component {...props} />
    </MuiThemeProvider>
  );
}

stories/header.js:

import React from 'react';
import Header from '../Header';
import materialize from './materialize';
import { storiesOf, action } from '@kadira/storybook';

storiesOf('Header', module)
  .add('default button view', () => {
    const props = {
      label: "Button"
    }
    return materialize(Header, props);
  })
  .add('primary button view', () => {
    const props = {
      label: "Button",
      primary: true
    }
    return materialize(Header, props);
  });
ffxsam commented 8 years ago

@michaltakac No need for the materialize function. What you're doing is easily done via Storybook's built in addDecorator method:

import React from 'react';
import { action, storiesOf } from '@kadira/storybook';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';

import StarRating from '../common-ui/components/StarRating';

storiesOf('common.StarRating', module)
  .addDecorator(story => (
    <MuiThemeProvider muiTheme={getMuiTheme()}>
      {story()}
    </MuiThemeProvider>
  ))
  .add('summary mode', _ => (
    renderStarRating()
  ))
  .add('details mode', _ => (
    renderStarRating({ mode: 'details' })
  ))
  .add('rate mode', _ => (
    renderStarRating({ mode: 'rate' })
  ));

function renderStarRating(props) {
  const ratings = [
    { name: 'Bob G', rating: 5 },
    { name: 'Ali G', rating: 4 },
    { name: 'Frankenstein', rating: 1 },
    { name: 'Santa Claus', rating: 3 },
    { name: 'Another Person', rating: 4 },
    { name: 'Some Gal', rating: 5 },
    { name: 'Some Critical Dude', rating: 1 },
  ];

  return <div style={rootStyle}>
    <StarRating ratings={ratings} { ...props } />
  </div>
}

const rootStyle = {
  alignItems: 'center',
  display: 'flex',
  justifyContent: 'center',
  marginTop: 200,
};
michaltakac commented 8 years ago

Thank you for example code, helped!

vladfr commented 8 years ago

Shouldn't this be closed now that addDecorator exists?

arunoda commented 8 years ago

@vladfr I think yes.

Domiii commented 7 years ago

Hi!

Thank you for finding such an elegant solution to this annoying problem!

However, can the MUI support be generalized in a way that I can add the decorator only once, in the global config, instead of each individual story?

For reference, my .storybook/config.js looks like this:

import { configure } from '@kadira/storybook';
import { setStubbingMode } from 'react-komposer';

// See: https://github.com/kadirahq/react-komposer#stubbing
setStubbingMode(true);

function loadStories() {
  require('../client/modules/core/components/.stories');
  require('../client/modules/comments/components/.stories');
}

configure(loadStories, module);
usulpro commented 7 years ago

@Domiii You can add decorator both locally and globally:

import { addDecorator } from '@kadira/storybook';
import { muiTheme } from 'storybook-addon-material-ui';
// You can add decorator globally:
addDecorator(muiTheme());
Domiii commented 7 years ago

@UsulPro Does this work for you with the latest version?

When visiting the storybook frontend, I get:

ERROR in ./~/react-material-color-picker/dist/ic_done_black_64dp_1x.png
Module parse failed: D:\code\meteor\mantra-sample-blog-app\node_modules\react-material-color-picker\dist\ic_done_black_64dp_1x.png Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type.
SyntaxError: Unexpected character '�' (1:0)
    at Parser.pp$4.raise (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:2221:15)
    at Parser.pp$7.getTokenFromCode (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:2756:10)
    at Parser.pp$7.readToken (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:2477:17)
    at Parser.pp$7.nextToken (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:2468:15)
    at Parser.parse (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:515:10)
    at Object.parse (D:\code\meteor\mantra-sample-blog-app\node_modules\acorn\dist\acorn.js:3098:39)
    at Parser.parse (D:\code\meteor\mantra-sample-blog-app\node_modules\webpack\lib\Parser.js:902:15)
    at DependenciesBlock.<anonymous> (D:\code\meteor\mantra-sample-blog-app\node_modules\webpack\lib\NormalModule.js:104:16)
    at DependenciesBlock.onModuleBuild (D:\code\meteor\mantra-sample-blog-app\node_modules\webpack-core\lib\NormalModuleMixin.js:310:10)
    at nextLoader (D:\code\meteor\mantra-sample-blog-app\node_modules\webpack-core\lib\NormalModuleMixin.js:275:25)
 @ ./~/react-material-color-picker/dist/MaterialColorPicker.js 39:29-67
arunoda commented 7 years ago

@Domiii your issue is your webpack config can't understand png files. Our default setup should support it. But if you are using a custom webpack loader, try to use a proper loader for png files.

caub commented 6 years ago

I did an horrible:

const story = (...args) => ({
    add(name, fn) {
        storiesOf(...args).add(name, () => <Wrapper>{fn()}</Wrapper>);
        return this;
    },
});

same usage than before:

configure(() => {
    story('StatusChip', module)
        .add('with status', () => <StatusChip status="Rejected" />)
        .add('with progress', () => <StatusChip progress={72} />);
})

It would be great to allow to configure storiesOf, I'll see if I can make a PR later, because passing a wrapper, or an extra function every time is too much (@michaltakac)

Hypnosphi commented 6 years ago

@caub this should do effectively the same:

import { addDecorator } from '@storybook/react'

addDecorator(fn => <Wrapper>{fn()}</Wrapper>)

See https://storybook.js.org/basics/writing-stories/#using-decorators

AlexVotry commented 5 years ago

I'm using "useContext(user)" in my component and use that variable to populate the component. How would I be able to get that data into the storybook? I can't useContext without a functional component, so I don't know how to access that data in the storybook? I looked everywhere to see an example, but couldn't find it.

Hypnosphi commented 5 years ago

@AlexVotry

I can't useContext without a functional component

Yeah you definitely need one:

const Example = () => {
  const user = useContext(UserContext)
  return <MyComponent user={user} />
}

storiesOf('MyComponent', module).add('default', () => <Example />)

Or, maybe even better, useContext directly in MyComponent