How to map enum to select dropdown in Storybook? - javascript

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.
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?

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'],
},
}

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 }), {})

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" ]

"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.

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,
}

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>

Related

How to use reduce with typescript to populate a typed array?

I have an array of objects which is populated with objects generated by a reduce loop. These objects already have a type, hence I need the object generated by reduce to have the same type, but I'm strugling with the initial value of the reduce, since it is an empty object.
How can I set a type to the initial object without getting an error saying that the object is missing properties?
Interface and base value:
const productFormLocal = [
{
field: 'id',
config: {
elementType: false,
value: '',
validation: {},
valid: true,
touched: false
}
},
{
field: 'nome',
config: {
elementType: 'input',
elementConfig: {
type: 'text',
placeholder: 'Nome do Produto',
name: 'nome',
},
label: 'Nome do Produto',
value: '',
validation: {
required: true,
},
valid: false,
touched: false
}
}
]
interface ProductsList {
id: number
nome: string
qtde: number
valor: number
valorTotal: number
}
const productsList: ProductsList[] = []
For instance, if I do that I get the reduce working fine, but I wouldn't be able to push the generated objects to the array:
const data: Record<string, any> = {}
const productValues = productFormLocal.reduce((obj, item) => (
obj[item.field] = item.config.value, obj
), data)
productsList.push(productValues)
And if I do that I would get an error saying that data is missing the ProductList properties:
const data: ProductsList = {}
const productValues = productFormLocal.reduce((obj, item) => (
obj[item.field] = item.config.value, obj
), data)
productsList.push(productValues)
How could I solve this? I looked a lot but I coudn't find a way to get this right.
I know I could set all properties of ProductsList as optional, but it doesn't seem to be the best approach.
Set the reduce generic type, which will type the default value as well as the reducer function return type.
const productValues = productFormLocal.reduce<Record<string, any>>((acc, item) => ({
...acc,
[item.field]: item.config.value
}), {})

Return all values of nested arrays using string identifier

Given an object searchable, is there a simple way of returning all the id values using lodash or underscore.js (or equivalent) where I can define the path to id?
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
I am looking to see if this is possible in a manner similar to how we can use lodash.get e.g. if we wanted to return the things array from searchable we could do
const things = _.get(searchable, 'things');
I can't seem to find anything similar in the documentation. I am looking for something
that could contain an implementation similar to:
_.<some_function>(searchable, 'things[].properties[].id')
Note: I am well aware of functions like Array.map etc and there are numerous ways of extracting the id property - it is this specific use case that I am trying to figure out, what library could support passing a path as a string like above or does lodash/underscore support such a method.
Found a solution using the package jsonpath
const jp = require('jsonpath');
const result = jp.query(searchable, '$.things[*].properties[*].id')
console.log(result);
// outputs: [ 'd1-i1', 'd1-i2', 'd2-i1', 'd2-i2' ]
you can do it easily in plain js
like this
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
const search = (data, k) => {
if(typeof data !== 'object'){
return []
}
return Object.entries(data).flatMap(([key, value]) => key === k ? [value]: search(value, k))
}
console.log(search(searchable, 'id'))
_.map and _.flatten together with iteratee shorthands let you expand nested properties. Every time you need to expand into an array, just chain another map and flatten:
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
// Let's say the path is "things[].properties[].id"
const result = _.chain(searchable)
.get('things').map('properties').flatten()
.map('id').value();
console.log(result);
<script src="https://cdn.jsdelivr.net/npm/underscore#1.13.4/underscore-umd-min.js"></script>

Best way to to pass the output of a function to child components in Vue

I have a parent component with a data object config as below:
data() {
return {
config: {
Groups: [
{
name: "A",
Types: [
{ mask: 1234, name: "Alice", type: 1},
{ mask: 5678, name "Bob", type: 1},
]
},
{
name: "B",
Types: [
{ mask: 9876, name: "Charlie", type: 2},
{ mask: 5432, name "Drake", type: 2},
]
}
],
},
Defaults: {
dummyBoolean: false,
dummyNumber: 1
}
}
}
}
There are also 2 child components that for each of them, I want to pass the Types array (within each elements of the Groups object) if each_element.name == child component's name.
What I've done so far is having a computed function for each of the components as follows (which is highly inefficient):
computed: {
dataSender_A() {
let array= []
this.config.Groups.forEach( element => {
if (element.name === "A") array = element.Types
});
return array
},
dataSender_B() {
let array= []
this.config.Groups.forEach( element => {
if (element.name === "B") array = element.Types
});
return array
},
}
I'm looking for a better alternative to make this happen (as I might have more child components) and two approaches I tried so far have failed.
Having only one computed function that takes the component's name as argument and can be passed like <child-component-A :types="dataSender('A')" /> <child-component-B :types="dataSender('B')" /> (As it throws error dataSender is not a function)
computed: {
dataSender: function(groupName) {
let array= []
this.config.Groups.forEach( element => {
if (element.name === groupName) array = element.Types
});
return array
},
}
Having the above function in methods and pass that as props to child components (As it passes the function itself, not the outputted array)
I'd appreciate any help on this.
The computed properties don't accept parameters that are involved in the calculation, In this case you could just use a method like :
methods: {
dataSender: function(groupName) {
let array= []
this.config.Groups.forEach( element => {
if (element.name === groupName) array = element.Types
});
return array
},
}

json-rules-engine processing array of objects

Any help would be appreciated !! I couldn't find an answer.
Given input facts to the engine such as -
const facts = {
cart: {
prop1: true,
prop2: "My Cart",
prop3: {
prop4: "North America"
},
products: [
{
category: 1,
classification: null
},
{
category: 2,
classification: null
},
{
category: 1,
classification: null
}
]
}
};
I want to be able to process each object in the products array and define a separate rule for each category and set the classification field for that category.
Something like -
let condition1 = {
all: [
{
fact: 'category',
operator: 'equal',
value: 1
}
]
};
let condition2 = {
all: [
{
fact: 'category',
operator: 'equal',
value: 2
}
]
};
let event1 = {
type: 'category 1 event'
// Update classification
};
let event2 = {
type: 'category 2 event'
// Update classification
};
How do I do this? Do i set the classification field in the event handler for that event? How do I access the object the category belongs to?
I tried using the below, but it selects all the category values from each object. I want to be able to process each object separately.
let condition = {
all: [
{
fact: 'cart',
operator: 'contains',
value: 1,
path: '$.products[*].category'
}
]
}
I cant add them as runTime facts either since you can have only 1 runtime fact and its value. (No Duplicates)

Recursively filter an array of objects according to an array of conditions

I have an array with objects that I want to filter according to an indefinite number of conditions passed as parameters.
Here's how I filter the array a with a array of conditions in hide ()
const statuses = [
{
id: 0,
name: 'archived'
},
{
id: 1,
name: 'coming',
hide: (...filterParam) => filterParam.every(rule => rule)
}
];
const filteredStatuses = statuses.filter(element => {
switch (element.id) {
case 1:
return !element.hide(this.isTopTabs());
// other cases with others logic
default:
return true;
}
});
Now if each object can have its own children object array like that:
const statuses = [
{
id: 'statuses',
name: 'treeName',
children: [
{
id: 1,
name: 'inProgress',
hide: (...filterParam) => filterParam.every(Boolean)
},
{
id: 2,
name: 'coming',
checked: false,
hide: (...filterParam) => filterParam.every(Boolean)
}
]
}
];
How I can recursively iterate the same way?
Do you have a better way to filter this array dynamically by avoiding the switch / case?
And finally how to type the rest parameters with a generic like that hide: <T>(...filterParam: Array<T>) => filterParam.every(Boolean) ?
How I can recursively iterate the same way?
const filterRecursively = elements => filterStatuses(elements).map(it => ({ ...it, children: it.children && filterRecursively(it.children) }));
Do you have a better way to filter this array dynamically by avoiding the switch / case?
Why? Whats wrong with the switch case? Isn't it fancy enough?
And finally how to type the rest parameters with a generic like that hide: (...filterParam: Array) => filterParam.every(Boolean) ?
<T>(...filterParam: Array<T>) => boolean

Categories

Resources