storybook icon indicating copy to clipboard operation
storybook copied to clipboard

(addon-controls) Support destructering object properties into individual controls

Open pimlie opened this issue 4 years ago • 47 comments

Is your feature request related to a problem? Please describe.

Im often passing objects to my (Vue) components which by default will be added as JSON.stringified text control by @storybook/addon-controls. As I feel this is confusing/limited functionality (also because the JSON object isnt pretty printed (ie JSON.stringify(obj, null, 2)) I'd like to destructure my object properties into separate controls for each individual object property.

I have currently implemented this as follows in this stripped-down User example below

User.vue
// User.vue
<template>
  <p>The name of the user is: {{ user.name }}</p>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    }
  }
}
</script>
User.story.js
import User from '../User.vue'

export default {
  title: 'User',
  component: User,
  argTypes: {
    user_name: {
      control: 'text',
      defaultValue: 'pimlie',
      table: {
        category: 'user',
        type: {
          summary: 'text',
        }
      }
    }
  }
}

const Template = (args, { argTypes }) => {
  console.log(argTypes)
  return {
    components: {
      User
    },
    props: Object.keys(argTypes),
    watch: {
      user_name: {
        immediate: true,
        handler(value) {
          // use $set to ensure reactivity in case name
          // prop doesnt exist on this.user yet
          this.$set(this.user, 'name', value)
        }
      }
    },
    template: '<User :user="user" />',
  }
}

export const Primary = Template.bind({});
Primary.args = {
  user: {
    name: ''
  }
}

(Note: in reality I have abstracted creating the argTypes & watchers to also easily support nested objects, eg to support a dot-path like blog.message.user.name)

Describe the solution you'd like I would like that addon-controls would have improved support for the above (or a similar) strategy as the current approach of stringifying any object prop doesnt seem very user friendly

Also the above approach has several disadvantages:

  • in your ArgsTable you will have both a user control with type object as a user_name control with type text
  • the actual values of these 2 controls are in sync, so if you change the value of user_name the rendered component will show the new value. But the stringified value in the user object control will not be in sync, it will only show the second to last update. To explain by example:
    • Start with user & user_name both with value pimlie for name
    • Change user_name control to pimlie2
    • The rendered component updates to pimlie2, but the user object control will still show pimlie
    • Change user_name control to pimlie again
    • The rendered component updates to pimlie, but the user object control will now show pimlie2
  • atm it seems you are unable to change the name of a custom argType in the table. See screenshot below, you can group the user_name prop but I would also like the label in the ArgTable to read just name instead of user_name

Describe alternatives you've considered see above

Are you able to assist bring the feature to reality? sure, I can help with testing

Additional context image

pimlie avatar Aug 17 '20 11:08 pimlie

@shilman So what you think about my solution described in #12362? I can maybe PR that, if you accept my specs

Gieted avatar Sep 03 '20 11:09 Gieted

@Gieted I like the proposal, but I need a few days to let it roll around in my head. Thanks for thinking the problem through and being patient about this!

shilman avatar Sep 05 '20 01:09 shilman

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!

stale[bot] avatar Oct 04 '20 04:10 stale[bot]

What do you think about this as a workaround: https://github.com/storybookjs/storybook/pull/12685

shilman avatar Oct 08 '20 02:10 shilman

By default, each field in a component's args is rendered as a single row. If a field is a complex type, it will get rendered using a JSON editor per #12599 (existing behavior but better UX).

As an opt-in, the user can also specify a custom argType:

export default {
  title: ...,
  component: ...,
  argTypes: {
    foo: { table: { expanded: true } }
  }
}

In this case, the ArgsTable will generate an extra row for each field of foo with design TBD.

shilman avatar Oct 22 '20 01:10 shilman

I duplicate our use-case from https://github.com/storybookjs/storybook/issues/12362

This is exactly what we need in our case where we feed our components with VOs wrapper around real data (JSON) with additional properties from other parts of the system, or precomputed (so component won't know about the store or any data transformation logic):

So VO looks like this:

class OrganizationInfoVO {
  constructor(data, isDefaultOrganization, isCurrentUserOrganization) {
    this.data = data;
    this.isDefaultOrganization = isDefaultOrganization;
    this.isCurrentUserOrganization = isCurrentUserOrganization;
  }

  get id() { return this.data.id; }

  get name() { return this.data.name; }

  get country() { return this.data.country; }

  get picture() { return this.data.picture; }

  get count() { return this.data.count; }
}

export default OrganizationInfoVO;

And it would be great to have a way of changing properties or visualize them in storybook panels. I think having at least some kind of "update callback" from control which called before component re-render would be great.

VladimirCores avatar Oct 22 '20 15:10 VladimirCores

By default, each field in a component's args is rendered as a single row. If a field is a complex type, it will get rendered using a JSON editor per #12599 (existing behavior but better UX).

As an opt-in, the user can also specify a custom argType:

export default {
  title: ...,
  component: ...,
  argTypes: {
    foo: { table: { expanded: true } }
  }
}

In this case, the ArgsTable will generate an extra row for each field of foo with design TBD.

@shilman I'm not seeing this functionality. I have the custom argTypes configuration as listed above, but it is still showing the JSON editor instead of a row for each field.

jpmarra avatar Dec 08 '20 21:12 jpmarra

@jpmarra the issue is still open, meaning that it hasn't been added to storybook yet

shilman avatar Dec 08 '20 21:12 shilman

@shilman Ah I misread, my bad!

jpmarra avatar Dec 08 '20 21:12 jpmarra

Also really wanting this feature for a client. It seems like an obvious one. Passing a large object as a prop happens quite often. Or arrays of objects as well. That would another good one.

  1. An object prop - A control for editing properties of objects.
  2. An array of objects prop A control for editing adding an object to the array and then editing properties of the objects.

iamtomhanks avatar Dec 15 '20 18:12 iamtomhanks

please, this is such a great improvement

isc30 avatar Dec 23 '20 19:12 isc30

🙏

annacv avatar Jan 29 '21 09:01 annacv

It would be good if restructuring will be happening from TypeScript annotations.

VladimirCores avatar Feb 17 '21 15:02 VladimirCores

Waiting on it

samuelpietra avatar Feb 18 '21 00:02 samuelpietra

We'll be iterating https://github.com/storybookjs/storybook/pull/12824 and releasing in 6.2 as a short-term improvement in this space. We definitely see value in making the destructured UI as requested here, but don't have the bandwidth to handle it now. If anybody wants to take it on, I'd be happy to advise.

shilman avatar Feb 18 '21 01:02 shilman

@shilman What happened to #12362?

franktopel avatar Sep 17 '21 15:09 franktopel

Hey! I've found workaround for this case. I'm using flat to flatten args and unflatten flattenedArgs.

Follow the below code example.

import React from 'react';
import { flatten, unflatten } from 'flat';
import { ComponentStory } from '@storybook/react';
import MyComponent from './MyComponent';

const delimiter = '__';

const getControlInfo = (name: string) => {
  const [_root, group, subcategory, keyName] = name.split(delimiter);

  return {
    table: {
      category: group,
      subcategory: subcategory,
    },
    group,
    subcategory,
    keyName,
  };
};

const getBaseControlInfo = (name: string) => {
  const controlInfo = getControlInfo(name);
  return {
    name: controlInfo.keyName,
    table: controlInfo.table,
  };
};

export default {
  title: 'MyComponent',
  component: MyComponent,
};

const Template: ComponentStory<any> = (flattenedArgs) => {
  const args: any = unflatten(flattenedArgs, { delimiter });

  return <MyComponent {...args} />;
};

export const Primary = Template.bind({});
Primary.args = flatten(
  {
    root: {
      group1: {
        subcategory1: {
          name: 'ashnamuh',
        },
        subcategory2: {
          key: 'value!',
        },
      },
      group2: {
        subcategory3: {
          foo: 'bar',
        },
      },
    },
  },
  { delimiter }
);

Primary.argTypes = {
  root: {
    table: { disable: true },
  },
  // self
  root__group1__subcategory1__name: {
    ...getBaseControlInfo('root__group1__subcategory1__name'),
    // Add your argTypes options
  },
  root__group1__subcategory2__key: {
    ...getBaseControlInfo('root__group1__subcategory2__key'),
    // Add your argTypes options
  },
  root__group2__subcategory3__foo: {
    ...getBaseControlInfo('root__group2__subcategory3__foo'),
    // Add your argTypes options
  },
};

ashnamuh avatar Feb 11 '22 15:02 ashnamuh

The solution doesn't work with autogenerated controls. Would be nice to have something native, coming from storybook

gpessa avatar Apr 29 '22 09:04 gpessa

@ashnamuh thanks for the suggestion Flattening and unflattening worked wonders for my nested objects ❤️

AthiNos avatar Sep 16 '22 09:09 AthiNos

Is there any progress with this?

AmirTugi avatar Nov 16 '22 09:11 AmirTugi

would also love this feature 🥺🥺🥺

helloromero avatar Nov 23 '22 11:11 helloromero

This seems so essential! I was shocked when reading the docs that this is not available :(

BrendanRomanDev avatar Nov 30 '22 00:11 BrendanRomanDev

Hi. Any update on this @shilman ? It would be very valuable indeed

tinesoft avatar Jan 05 '23 08:01 tinesoft

I'm facing the same problem.

Unlike most people, my aim is to specify a nested property as an action in Storybook.

Either way, I haven't found anything in the docs or heard about a fix for this coming soon.

Victor-Nyagudi avatar Mar 06 '23 12:03 Victor-Nyagudi

[...] my aim is to specify a nested property as an action in Storybook. [...]

@Victor-Nyagudi, I faced the same problem recently and found a way to solve it. It's not an automatic way to fix it, but it worked for me.

First, import the action factory from the Storybook action library:

import { action } from '@storybook/addon-actions'

Then, in the arguments of your story, create an action by calling the factory method, passing it the name you want to be displayed in Storybook when calling the action:

const onChangeAction = action('onChange') // the name 'onChange' can be anything you want

Finally, run the function you created earlier, passing it the values you want to display in Storybook:

// const value = "foo"
onChangeAction({ value }) // this will display 'onChange { value: "foo" }' in Storybook

Example:

import { action } from '@storybook/addon-actions'

// [...]

const SomeExampleStory = // ...

SomeExampleStory.args = {
  someDeepObjectProp: {
    onChange({ target: { value } }) {
      const onChangeAction = action('onChange')
      onChangeAction({ value })
    }
  },
  // ...other props...
}

Of course, you need @storybook/addon-actions to be installed as a dependency in your project, or @storybook/addon-essentials (because @storybook/addon-actions is part of it).

⚠️ IMPORTANT: Please do not install @storybook/addon-actions and @storybook/addon-essentials at the same time or it will not work.

javimsevilla avatar Apr 10 '23 13:04 javimsevilla

+1 for this feature

sw-tracker avatar Jun 12 '23 06:06 sw-tracker

+1

gpessa avatar Jun 12 '23 07:06 gpessa

+1

younessadmi avatar Jul 26 '23 10:07 younessadmi

+1

Tech-Code1 avatar Jul 31 '23 03:07 Tech-Code1

+100000

jen-n-taylor avatar Aug 04 '23 16:08 jen-n-taylor