storybookjs / storybook

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

Using args/controls to change vue's local state #12073

Open martijnvanloon opened 4 years ago

martijnvanloon commented 4 years ago

I was under the impression that with storybook 6 I can change values in vue's data() property without my component using props. I Use TS and have declared two data properties: userMenuOpen and text. The code below reflects what i want to do. If it's not possible like with 5 I misunderstood.

Vue, typescript, storybook 6

Component:

export default class TopNavBar extends Vue {
  userMenuOpen: boolean = false;
  text: string = 'hello';
}

Story:

export default {
  title: 'nav/TopNavBar',
  excludeStories: /.*Data$/,
  decorators: [StoryRouter()],
  component: TopNavBar,
  argTypes: {
    userMenuOpen: { control: 'boolean' },
    text: { control: 'text' },
  },
};

const Template = (args: any, { argTypes }: any) => ({
  components: { TopNavBar },
  data() {
    return {
      userMenuOpen: args.userMenuOpen,
      text: args.text,
    };
  },
  template: '<top-nav-bar  />',
});

export const Rounded = Template.bind({});
Rounded.args = {
  userMenuOpen: true,
  text: 'A different text',
};
github-actions[bot] commented 4 years ago

Automention: Hey @backbone87 @pksunkara @pocka, you've been tagged! Can you give a hand here?

shilman commented 4 years ago

I don't know much about vue so I can't answer your question fully. However, I can answer partially and hopefully one of our community vue experts can take it the rest of the way.

Here's a typical template:

https://github.com/storybookjs/storybook/blob/master/examples/vue-kitchen-sink/src/stories/addon-controls.stories.js#L18-L22

The important part is props: Object.keys(argTypes). This is a list like, in your case, ['userMenuOpen', 'text']. When you add this in a story function, @storybook/vue automatically and transparently pulls them out of the args object and makes them available as dynamically updating props in your story.

How to further transform those props in the data() function, etc., is outside of my vue knowledge, but I assume that it's something typical/standard. Hope that helps!

Aaron-Pool commented 4 years ago

@martijnvanloon just so I fully understand, what's your objective in using data rather than props in your story components?

wallslide commented 4 years ago

I think I understand where @martijnvanloon 's confusion is coming from. The args are being automatically passed as props to the component you are exporting as a story. So you don't have to add them to the component's data() block, since they are passed in as props and accessible from the component already. If you don't either manually setup that component to expect those args as props, or use the automatic props: Object.keys(argTypes) method, then I assume they will be living under this.$attrs where all bound variables that aren't explicitly defined as props live.

martijnvanloon commented 4 years ago

Thank you for your help so far! Wallslide is onto something, I like and need to build components that have local state "data()". For example a "status" boolean that changes based on the results of an api call made by the component itself and that will display a "success" text (without sending events and props between "container" and "presentational" components).

The args passed with storybook indeed end up in the this.$attrs variable. To use these in the component I need to add code to my components reassigning the this.$attrs values on to my actual values.

I have written a working example below in the mounted hook:

When storybook is running it checks the "this.$attrs" variable It checks if keys in this object exist in the local Vue state and if they do it sets them it repeats this proces continually

Component:

  data: function () {
    return {
        textExample: 'empty';
    }
  },

  mounted() {
    if (window.__STORYBOOK_ADDONS) {
      setInterval(() => {
        Object.entries(this.$attrs).forEach(([key, value]) => {
          if (this[key] && this[key] !== value) {
            this[key] = value;
          }
        });
      }, 500);
    }
  }

Story example

export default {
  title: 'nav/TopNavBar',
  component: TopNavBar,
  argTypes: {
    textExample: { control: 'text' },
  },
  args: {
    textExample: 'succesfully changed',
  },
};

const Template = (args: any, { argTypes }: any) => ({
  components: { TopNavBar },
  template: '<top-nav-bar :textExample="textExample"  />',
  props: Object.keys(argTypes),
});

The above example gives me full "live" control over the data properties in my component with storybook controls. However adding code to my components that is only used for testing in storybook (and will bloat my application) seems like a bad practice. I could add this code globally to each component and make sure it does not compile to live, that would work pretty well.

martijnvanloon commented 4 years ago

Adding this mixin to storybooks preview.js checks if there are unspecified attributes supplied by a story and adds them to the components data property (if they exist). It requires no extra code in the components.

I came to realize it is often better to use props, but this is a good alternative for when props are not a fiting solution.

// bind non prop attributes to their corresponding data property
Vue.mixin({
  mounted: function() {
    if (Object.entries(this.$attrs).length > 0) {
      this.intervalSb = setInterval(() => {
        Object.entries(this.$attrs).forEach(([key, value]) => {
          if (this[key] && this[key] !== value) {
            this[key] = value;
          }
        });
      }, 500);
    }

  },
  destroyed() {
    clearInterval(this.intervalSb);
  }
});
elevatebart commented 4 years ago

If you want to keep rectivity, you might what to replace

this[key] = value;

with

this[key] = Vue.observable(value);
martijnvanloon commented 4 years ago

If you want to keep rectivity, you might what to replace

this[key] = value;

with

this[key] = Vue.observable(value);

Thanks! However when I do this and remove the interval from my code, reactivity (adjusting a control and seeing results) only works when in storybooks "Docs" tab but not the "Canvas" tab.

stale[bot] commented 4 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

sumerokr commented 4 years ago

change values in vue's data() property without my component using props.

Hi!. Probably you confused by 2 things:

So the simplest way is:

That was an "A-HA" moment for me as well and I spend too much time before realizing that.

sumerokr commented 4 years ago

Hey, I was half wrong, half right. While you can use the approach described above to simply pass data to the target component, you need to do some tricks to make it work with controls addon. Pay attention, you have to receive args in story function as an argument, even if you don't need it. I use explicit arg names in example to make it clear, where data comes from.

export const StoryName = (args) => ({
  components: { TheUser },
  template: `<TheUser :name="nameFromArgs"></TheUser>`,
  props: ["nameFromArgs"]
});

Default.args = {
  nameFromArgs: "Leonardo!"
};

If you want to pass more arguments, you can use Object.keys() to fill the props.

export const StoryName = args => ({
  components: { TheUser },
  template: `<TheUser :name="nameFromArgs" :age="ageFromArgs" :gender="genderFromArgs"></TheUser>`,
  props: Object.keys(args)
});

StoryName.args = {
  nameFromArgs: "Leonardo!",
  ageFromArgs: 35,
  genderFromArgs: "male"
};

You can go further and here is the option to auto-bind all the passed args to component properties. Pay attention, that you have to use exact prop name in args this time.

export const StoryName = args => ({
  components: { TheUser },
  template: `<TheUser v-bind="allPropsFromArgs"></TheUser>`,
  props: {
    allPropsFromArgs: {
      default: () => args
    }
  }
});

StoryName.args = {
  name: "Leonardo!",
  age: 35,
  gender: "male"
};
doutatsu commented 3 years ago

Still quite frustrated at the inability to change the internal state of the Vue component in Storybook. I don't want to introduce a prop, just for the sake of StoryBook - that's a smell to me, as it's a prop without use, except for StoryBook. I would really like there to be an ability to control the internal state, just like you do with props

maxKimoby commented 3 years ago

Is there any development on this issue?

doutatsu commented 2 years ago

Back here to bump this issue up - not having access to the internal data of the component continues to be a major issue with using Storybook...

Envoy49 commented 2 years ago

I've spend more than half day looking for the solution which is not possible at the moment, it is sad that Storybook has proper integration only with React and Vue support is lagging.

AndreiSoroka commented 2 years ago

have same situation

ndelangen commented 1 year ago

Just a suggestion, but you could consider using play functions: https://storybook.js.org/docs/react/writing-stories/play-function#page-top

K-Schaeffer commented 1 year ago

Play functions are indeed a great solution to a lot of scenarios, but there are still some cases when we need to force some value on a data property or override some method, and this is not currently feasible.

Crone1331 commented 1 year ago

still relevant

nick-0101 commented 9 months ago

still looking...

kasperpeulen commented 8 months ago

@larsrickert @chakAs3 Any ideas how we could implement this issue?

larsrickert commented 8 months ago

@larsrickert @chakAs3 Any ideas how we could implement this issue?

To be honest I don't really get the issue here. In my opinion the described behavior is how it should behave.

  1. As a general design pattern for components, props should be used to pass data to a component
  2. As far as I know, it is not possible to directly access component internal data from the outside since Vue 3 (at least for .vue components). Only if you explicitly expose them with defineExpose() you are able to access them. So this limitation seems to be done by design from the Vue team so I don't think we should find a workaround for this.

My suggestion would be to use an atomic component architecture like this:

Please correct me if I got the point wrong 😄

There is a general design pattern for this called Atomic design

chakAs3 commented 8 months ago

I fully agree with Lars. Manipulating internal data is generally discouraged in state management, especially within Vue. Even accessing component data should be explicitly exposed. Perhaps we should explore safer methods like unit testing or functional component with initial state.

I'm still open to find a proper implementation to achieve the final goal, i need to see an example for internal state manipulation use case.