Storybook web-components: sending array as component property - javascript

I'm using Storybook 6.5.9 to render web components (made with Stencil). I have several of them working correctly but now I'm creating a story for a new component that can receive a property that is an array of objects.
The component would be something like this:
import {Component, Host, h, Prop} from '#stencil/core';
#Component({
tag: 'test-component',
})
export class TestComponent {
/**
* Array of options for the group.
*/
#Prop() items?: String[] = [];
render() {
return (
<Host>
{
this.items.map(item => (
<h1>{item}</h1>
))
}
</Host>
)
}
}
And this is the story:
import { Story, Meta } from '#storybook/html';
export default {
title: 'Components/TestComponent',
parameters: {
options: {
showPanel: true
},
}
} as Meta;
const Template: Story = (args) => {
return `
<test-component items="${['some', 'thing']}">
</test-component>
`;
};
export const TestComponent: Story = Template.bind({});
I have tried setting the items property to a string but the component never gets anything, it's always an empty array.
I'm not getting any errors either in the console. I'm definitely doing something wrong but I don't know what it is... I've been using several other data types for those properties (boolean, string, numbers...) but this is the first time I'm using objects/arrays and I'm not able to get it to work.
Any help will be highly appreciate it.
Thanks!

Properties are passed in as strings in HTML.
Use JSON.parse(this.items) in your render() method in case this.items is not an array:
import { Component, Host, h, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
})
export class MyComponent {
/**
* Array of options for the group.
*/
#Prop() items?: string | string[] = '';
render() {
return (
<Host>
{(Array.isArray(this.items) ? this.items : JSON.parse(this.items)).map(item => (
<h1>{item}</h1>
))}
</Host>
);
}
}
For that to work, you need to pass in your items as valid JSON, meaning you have to use single attribute quotes and double quotes for the "strings" in the "array":
const Template: Story = (args) => `
<test-component items='${["some", "thing"]}'>
</test-component>
`;
If you are receiving [object Object] in your component, try to stringify your prop value before passing:
const Template: Story = (args) => `
<test-component items='${JSON.stringify(["some", "thing"])}'>
</test-component>
`;

You can not set arrays or objects as properties via HTML (only in JSX/TSX). You have two options:
pass them as a JSON string and parse them back into an array/object; you’d basically have to type them as a string in your component, and then cast the type after parsing them
set the property via a script; there’s two options, depending on whether or not the option is required or not.
<test-component></test-component>
<script>
document.querySelector("test-component").items = ["foo"]
</script>
or if the prop is required:
<script>
const testComponent = document.createElement("test-component")
testComponent.items = ["foo"]
document.body.appendChild(testComponent)
</script>

Related

Passing string literal as a single prop in Typescript + React

It seems this concept is so basic, there's a lack of documentation about it. I can pass objects as props, but can't seem to pass a basic string literal.
Functional Component
I have a functional component that takes a typed prop, like so:
const ChildComponent = (name: string) => {
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>
{ name }
</p>
</div>
);
}
and call it like so:
<ChildComponent name="testName" />
Error
VSCode throws the error on ChildComponent:
Type '{ name: string; }' is not assignable to type 'string'
I'm very new to Typescript, but from what I can tell, it's reading the string literal as an object.
Possible Solutions
Much of what I've read advises creating a custom typed prop, even for a single property, like so:
Type: NameProp {
name: string
}
and using that as the prop type, instead of string.
Isn't this overkill? Or am I missing something very basic.
const ChildComponent = ({ name }: { name: string }) => {
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>{name}</p>
</div>
);
};
You have to destructure it from props object.
props is an object.
CODESADNBOX LINK
const ChildComponent = (props: ChildComponentProps) => {
const { name } = props; // CHANGE THAT YOU HAVE TO DO
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>{name}</p>
</div>
);
};
or
const ChildComponent = ({ name }: ChildComponentProps) => {
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>{name}</p>
</div>
);
};
where ChildComponentProps is
interface ChildComponentProps {
name: string;
}
Define a prop interface the define the props parameter as object :
interface Props{
name:string
}
const ChildComponent: React.FC<Props> = ({name}) => {
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>
{ name }
</p>
</div>
);
}
Props supposed to be an object. You are not the only one using this prop object.
Note that JSX is just a syntax extension use to make it easy to read & code components.
But this code will translate into the pure javascript code,
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
The above code will translate into before execute,
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
Later this createElement will also return an object which will use to build the whole element tree of your application.
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
Notice the children property ? Its also a prop which was not added by you but the React it self.
NOTE: Above code snippets are directly copied from React JS documentation.
So to answer your question,
You cannot change the type of the props object. It must be an object. The reason why you seen that error message is because you forcefully telling the type of props is an string but actually it is not.
The way you should do it is,
const ChildComponent: React.FC<{ name: string }> = ({ name }) => {
return (
<div className={styles.childComponent}>
<p className={styles.styledName}>
{ name }
</p>
</div>
);
}

array of html elements or strings typescript [duplicate]

Using React with TypeScript, there are several ways to define the type of children, like setting it to JSX.Element or React.ReactChild or extending PropsWithChildren. But doing so, is it possible to further limit which particular element that React child can be?
function ListItem() {
return (
<li>A list item<li>
);
}
//--------------------
interface ListProps {
children: React.ReactChild | React.ReactChild[]
}
function List(props: ListProps) {
return (
<ul>
{props.children} // what if I only want to allow elements of type ListItem here?
</ul>
);
}
Given the above scenario, can List be set up in such a way that it only allows children of type ListItem? Something akin to the following (invalid) code:
interface ListProps {
children: React.ReactChild<ListItem> | React.ReactChild<ListItem>[]
}
You can't constrain react children like this.
Any react functional component is just a function that has a specific props type and returns JSX.Element. This means that if you render the component before you pass it a child, then react has no idea what generated that JSX at all, and just passes it along.
And problem is that you render the component with the <MyComponent> syntax. So after that point, it's just a generic tree of JSX nodes.
This sounds a little like an XY problem, however. Typically if you need this, there's a better way to design your api.
Instead, you could make and items prop on List which takes an array of objects that will get passed as props to ListItem inside the List component.
For example:
function ListItem({ children }: { children: React.ReactNode }) {
return (
<li>{children}</li>
);
}
function List(props: { items: string[] }) {
return (
<ul>
{props.items.map((item) => <ListItem>{item}</ListItem> )}
</ul>
);
}
const good = <List items={['a', 'b', 'c']} />
In this example, you're just typing props, and List knows how to generate its own children.
Playground
Here's a barebones example that I am using for a "wizard" with multiple steps. It uses a primary component WizardSteps (plural) and a sub-component WizardStep (singular), which has a "label" property that is rendered in the main WizardSteps component. The key in making this work correctly is the Children.map(...) call, which ensures that React treats "children" as an array, and also allows Typescript and your IDE to work correctly.
const WizardSteps: FunctionComponent<WizardStepsProps> & WizardSubComponents = ({children}) => {
const steps = Children.map(children, child => child); /* Treat as array with requisite type */
return (
<div className="WizardSteps">
<header>
<!-- Note the use of step.props.label, which is properly typecast -->
{steps.map(step => <div className="WizardSteps__step">{step.props.label}</div>)}
</header>
<main>
<!-- Here you can render the body of each WizardStep child component -->
{steps.map(step => <div className="WizardSteps__body">{step}</div>)}
</main>
</div>
);
}
const Step: FunctionComponent<WizardStepProp> = ({label, onClick}) => {
return <span className="WizardSteps__label">
{label}
</span>
}
WizardSteps.Step = Step;
type WizardSubComponents = {
Step: FunctionComponent<WizardStepProp>
}
type WizardStepsProps = {
children: ReactElement<WizardStepProp> | Array<ReactElement<WizardStepProp>>
};
type WizardStepProp = {
label: string
onClick?: string
children?: ReactNode
}
Absolutely. You just need to use React.ReactElement for the proper generics.
interface ListItemProps {
text: string
}
interface ListProps {
children: React.ReactElement<ListItemProps> | React.ReactElement<ListItemProps>[];
}
Edit - I've created an example CodeSandbox for you:
https://codesandbox.io/s/hardcore-cannon-16kjo?file=/src/App.tsx

How to have generic typescript props in react? Typescript generic props don't work

Here's what I'm trying to do in react,
I've got a functional component where I pass down 1 prop
<TableComponent tableStateProp={tableState} />
tableState is a state hook in the parent component
const [tableState, setTableState] = useState<TableState<string[]>>();
the table state type is defined inside of my table component
export type TableState<T> = {
pagination: {
limit: number,
skip: number,
}
data: T[];
columns: string[],
}
But here is where my problem starts, ideally I would be able to do this
const TableComponent: React.FC<{
tableState: TableState<T>;
}> = ({tableState}) => {
But I get an error saying TS2304: Cannot find name 'T'.
I know for a generic prop function the syntax is something like function<T>(): type<T>
but what is it for a generic prop/object?
Edit: I am using this component elsewhere where data is not a string[], hence why I'm trying to make it generic
Thank you
You don't need to use React.FC<>. Declare your component as a named function and you can add the generic <T>.
export type TableState<T> = {
pagination: {
limit: number;
skip: number;
};
data: T[];
columns: string[];
};
function TableComponent<T>({
tableState,
}: React.PropsWithChildren<{
tableState: TableState<T>;
}>) {
// ...
}
If you don't need the children prop to work, you don't need to use React.PropsWithChildren either, just:
function TableComponent<T>({ tableState }: { tableState: TableState<T> }) {
If you want to be explicit about the return typing right at the TableComponent level (and not when you use it in your app later on), you can peek at what React.FC is doing and type explicitly accordingly:
function TableComponent<T>({
tableState,
}: {
tableState: TableState<T>;
}): ReactElement<any, any> | null {
return null;
}
You need update like this:
const TableComponent: React.FC<{
tableState: TableState<string[]>;
}>

How to get text content of slot?

We can have stencilJS element with slot as below
<my-component>123</my-component>
I'm trying to get the value of 123 from my render method itself, wondering if that is possible?
#Component({ tag: 'my-component' })
export class MyComponent {
render() {
return (
<div><slot /></div>
)
}
}
I would like to do some string formatting on 123 instead of rendering slot directly
import { Element } from '#stencil/core';
#Component({ tag: 'my-component' })
export class MyComponent {
/**
* Reference to host element
*/
#Element() host: HTMLElement;
componentWillRender() {
console.log(this.host.innerHTML)
}
render() {
return (
<div><slot /></div>
)
}
}
In web components, the content inside of it is part of the main DOM, too. This content is not going to show if you don't use slots; but, the content is going to project next to the #shadow-root anyway (check it using the chrome developer tools in the "elements" section).
So, if you do not want to show the content using default slots, you can use the property decorator #Element() and declare a property of type HTMLElement:
Then, you can access to the content via innerHTML or innerText.
Finally, you can format the content. Check the code snippet bellow:
import { Component, Element, h } from "#stencil/core";
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() element: HTMLElement;
formatContent(content: any) {
if ( isNaN(content)){
// Your format here
return content;
} else {
return content + '.00';
}
}
render() {
return [
// Commented slot tag
// <slot></slot>,
<div> {this.formatContent(this.element.innerHTML)} </div>
];
}
}
Using three times the web component with 2 strings and a number as a entry data, the result should be:
My text
My text 2
123.00

How do I import JSX into .tsx file (not in React)?

I am not using React.
I am using Stenciljs.
I have the following .tsx file:
export class MyComponent {
#Prop() message: string;
render() {
return (<div>{this.message}</div>);
}
}
I want to do this instead:
import myTemplate from '../my-template.??';
export class MyComponent {
#Prop() message: string;
render() {
return (myTemplate);
}
}
with ../my-template.?? containing:
<div>{this.message}</div>
Is it possible and how ? Thanks in advance for any help :)
Yes, you can absolutely do this, there are just a couple of things you need to tidy up:
Main file
import { Template } from '../template'; // No need for file extension but we're using a named export so we need the curly braces around 'Template'
export class MyComponent {
#Prop() message: string;
render() {
return ( // You don't technically need the parentheses here as you're just returning one thing
<Template /> // When outputting an imported component, it goes in angle brackets and the backslash closes it like an HTML element
)
}
}
Template
import React from 'react'; // template needs React
export const Template = () => { // defining the export in this way is known as a named export
return (
<p>A message here</p>
)
}
Okay, so that's going to get you a message output which is from your template. However, you were asking about passing a message to that template for it to output. That's totally easy as well - you just need to get some props in there. Here is the modified version of the above:
Main file
import { Template } from '../template';
export class MyComponent {
#Prop() message: string;
render() {
return (
<Template messageToOutput={message} /> // The first argument is the name of the prop, the second is the variable you defined above
)
}
}
Template
import React from 'react';
export const Template = (props) => { // props are received here
return (
<p>{props.messageToOutput}</p> // props are used here
)
}
That's how you pass data around in React - hope that helps!

Categories