'How to map enum to select dropdown in Storybook?

I have a simple JS "enum" like this

const MyEnum = {
  Aaa: 1,
  Bbb: 84,
};

And I have a simple story:

import MyEnum from 'models/my-enum';
import HotSpot from 'hot-spot/hot-spot.vue';
import hotSpotProp from './hot-spot.stories.defaults';

export default {
  title: 'components/catalog/images/HotSpot',
  args: {
    hotspotProp: hotSpotProp,
    currentWidth: 360,
    selectedCallouts: [],
    calloutMode: true,
    originalWidth: 2100,
    title: 'Example tooltip',
  },
  argTypes: {
    oemId: {
      options: Object.keys(MyEnum), // an array of serializable values
      mapping: MyEnum, // maps serializable option values to complex arg values
      control: {
        type: 'select', // type 'select' is automatically inferred when 'options' is defined
        // labels: MyEnum,
      },
    },
  },
};

const Template = (args, { argTypes }) => ({
  components: { HotSpot },
  template: `<HotSpot v-bind="$props" />`,
  props: Object.keys(argTypes),
});

export const Default = Template.bind({});

Example from docs is not working.

I have a select dropdown working, but it returns a String instead of a Number from mapping.

Dropdown

I get an error in my storybook in the console:

[Vue warn]: Invalid prop: type check failed for prop "oemId". Expected Number with value NaN, got String with value "Aaa".

How to map enum to select dropdown in Storybook?



Solution 1:[1]

That storybook doc example is absolute horror. Here's an example that will instantly show you what to do.

myValueList: {
      options: [0, 1, 2], // iterator
      mapping: [12, 13, 14], // values
      control: {
        type: 'select', 
        labels: ['twelve', 'thirteen', 'fourteen'],
      },
    }

Solution 2:[2]

Enums end up as Objects, so:

enum Nums {
 Zero,
 One,
 Two,
 Three,
}

Seems to become an Object that looks like:

{
  0: "Zero",
  1: "One",
  2: "Two",
  3: "Three",
  One: 1,
  Three: 3,
  Two: 2,
  Zero: 0,
}

Since all object keys are strings or symbols in JavaScript, the only way I've been able to guarantee I only get the string values from an Enum is to use Object.values and filter strings:

    oemId: {
      options: Object.values(MyEnum).filter(x => typeof x === "string"),
      mapping: MyEnum,
      control: {
        type: 'select',
      },
    },

Or, filter out the keys and retain an Object - this way Storybook can still default the value without issues:

options: Object.entries(MyEnum)
    .filter(([, value]) => typeof value !== "string")
    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})

Solution 3:[3]

"Easy" (with this little helper):

function enumOptions(someEnum) {
  return {
    options: Object.keys(someEnum)
      .filter((key) => !isNaN(parseInt(key)))
      .map((key) => parseInt(key)),
    mapping: someEnum,
    control: {
      type: 'select',
      labels: Object.values(someEnum).filter(
        (value) => typeof value === 'string'
      ),
    },
  }
}

This can the be used in the OP's example code as follows:

export default {
  title: 'components/catalog/images/HotSpot',
  args: {
    oemId: MyEnum.MyValue  // default value for all stories can be used, 
                           // here "MyValue" will be preselected in dropdown
                           // (or individual `story.args` oemId value from MyEnum)
  },
  argTypes: {
    oemId: enumOptions(MyEnum)
  },
};

It is indeed surprising that this is not an out-of-the-box feature in storybook, requiring such a rather contrived workaround.

Thanks to @Anthony's and @Lee Chase's answers pointing in the right direction.

Solution 4:[4]

You are looking for Object.values not the .keys.

const MyEnum = {
    Aaa: 1,
    Bbb: 84,
};

Object.values(MyEnum); // -> [ 1, 84 ]
Object.keys(MyEnum);   // -> [ "Aaa", "Bbb" ]

Solution 5:[5]

I have given up on mapping for now and use a computed value, it pollutes the template a bit but a utility function or two can make it look a little tidier.

argTypes: {
    myValueList: {
        options: [0, 1, 2], // iterator
        control: {
            type: 'select', 
            labels: ['twelve', 'thirteen', 'fourteen'],
        },
    }
}
// .
// .
// .
const mappingMyValueList = [12, 13, 14];
// .
// .
// .
computed() {
    computedMyValueList() { 
      return () => mappingMyValueList[this.$props.myValueList];
    }
}
// .
// .
// .
<div>{{computedMyValueList}}</div>

Solution 6:[6]

The easiest way without any helper for me was:

export default {
  title: 'One/SingleBarItem',
  component: SingleBarItem,
  // ? Creates drowdown to select Phase enum values
  argTypes: {
    phase: {
      options: Object.values(NodeExecutionPhase),
      mapping: Object.values(NodeExecutionPhase),
      control: {
        type: 'select',
        labels: Object.keys(NodeExecutionPhase),
      },
    },
  },
} as ComponentMeta<typeof SingleBarItem>;

Where NodeExecutionPhase defined as:

enum Phase {
  UNDEFINED = 0,
  QUEUED = 1,
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jingle
Solution 2
Solution 3 Philzen
Solution 4 Filip Seman
Solution 5 Lee Chase
Solution 6 Nastya Rusina