I am writing a story in Storybook for the Stencil component. Seems like all props work as expected if they are all lowercase. However, if the prop is camelCase, it is not working. I tried both camelCase and 'dash-case' .nothing works.
Stencil component:
import {Component, Prop, h} from '#stencil/core';
#Component({
tag: 'text-input',
shadow: true,
})
export class TextInput {
#Prop() label: string;
#Prop() inputId: string;
render() {
return (
<div class="text-input">
<label htmlFor={this.inputId}>{this.label}</label>
<input type="text" id={this.inputId} />
</div>
)
}
}
Storybook story:
export default {
title: 'Components/Text Input',
argTypes: {
label: {control: 'text'},
inputid : {control: 'text'},
}
};
const Template = (args) => {
let tag = `<text-input label="${args.label}" inputId="${args.inputid}"></text-input>`;
return tag
};
export const Default = Template.bind({});
Default.args = {
label: 'First name:',
inputid: 'Input1'
};
inputId is the one that doesn't work while label is fine. Renaming inputId="${args.inputid}" to input-id="${args.inputid}"is not helping. It only works if I rename the Stencil prop to the all-lowercase inputid.
How to solve this issue? How can I keep Stencil props camelCase and make it work in Storybook?
Might be something to do with the casing of your argTypes and Default args, i.e. "inputid" is all lowercase. Maybe try the below?
export default {
title: 'Components/Text Input',
argTypes: {
label: {control: 'text'},
inputId : {control: 'text'},
}
};
const Template = (args) => {
let tag = `<text-input label="${args.label}" inputId="${args.inputId}"></text-input>`;
return tag
};
export const Default = Template.bind({});
Default.args = {
label: 'First name:',
inputId: 'Input1'
};
I would also try and create your template without string concatenation. EG:
const Template = ({ label, inputId }) => {
const element = document.createElement('text-input');
element.label = label;
element.inputId = inputId;
return rootEl;
};
This will make it easier when you pass in params of another type than string
Related
I'm trying to pass data to a custom vue component that gets rendered inside the tiptap editor. I can pass default properties but assigning reactive values to it doesn't seem to work.
This is the tiptap-node-extension.js file:
import {Node, mergeAttributes} from '#tiptap/core'
import {VueNodeViewRenderer} from '#tiptap/vue-3'
import Component from '#/views/components/vue-component.vue'
export default Node.create({
parseHTML() {
return [{ tag: 'vue-component' }]
},
renderHTML({ HTMLAttributes }) {
return ['vue-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return VueNodeViewRenderer(Component)
},
})
the script setup portion of the editor component:
<script setup>
import {useEditor, EditorContent, BubbleMenu} from '#tiptap/vue-3'
import StarterKit from '#tiptap/starter-kit'
import {Underline} from "#tiptap/extension-underline";
import {TextAlign} from "#tiptap/extension-text-align";
import {Link} from "#tiptap/extension-link";
import VueComponent from '#/js/tiptap-node-extension.js'
const editor = useEditor({
extensions: [
StarterKit,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
Underline,
Link,
VueComponent.extend({
name: 'vueComponent',
group: 'block',
draggable: true,
addAttributes() {
return {
src: {
default: '123',
}
}
},
}
),
],
content: props.modelValue,
onUpdate: ({ editor }) => {
emit('update:modelValue', editor.getHTML())
},
editable: props.locked ? false : store.admin
})
const sendDataToExtension = async (editor, event) => {
// Triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
</script>
and the vue component:
<script setup>
import {NodeViewWrapper} from '#tiptap/vue-3'
const props = defineProps({
node: {
type: Object,
required: true
},
updateAttributes: {
type: Function,
required: true,
}
})
</script>
<template>
<node-view-wrapper class="vue-component" data-drag-handle="">
<p>{{ node.attrs.src }}</p>
</node-view-wrapper>
</template>
The default of src gets through but when I try to assign a reactive object (that gets created after mounting the editor component) it ends up being undefined.
This works:
src: {
default: '123'
}
but this doesn't:
...
src: {
default: state.src
}
...
const sendDataToExtension = async (editor, event) => {
// triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
How do I send data to the vue component that is created after mounting editor?
Attempt:
editor.chain().focus().insertContent('<vue-component/>', {src: state.src}).run()
First I would say that I would recommend creating a purpose built extension, instead of having the general VueComponent that you have now. If you extend more based on that extension you will have several extension competing for the tag. Move all code that you set in extend to the actual extentions, you can set any tag-name you want.
Now to what I believe is the problem here: insertContent look like this:
insertContent: (value: Content, options?: {
parseOptions?: ParseOptions;
updateSelection?: boolean;
})
Content is declared as
export declare type Content = HTMLContent | JSONContent | JSONContent[] | null;
export declare type HTMLContent = string;
export declare type JSONContent = {
type?: string;
attrs?: Record<string, any>;
content?: JSONContent[];
marks?: {
type: string;
attrs?: Record<string, any>;
[key: string]: any;
}[];
text?: string;
[key: string]: any;
};
In your case you will have to add the src attribute to your html string, however I would recommend using the JSONContent type in your case then:
editor.chain().focus().insertContent({type: "vueComponent", attrs:{src: state.src}}).run()
Here the type is the name that you set of the component.
Hope this makes sense, the documentation on tiptap is kind of good as well https://tiptap.dev/guide/custom-extensions/#attributes
Let me know if you have further issues.
I built a Select component in react that is fully typed and I now added a multiple prop to it, that will change the typing of the value and the onChange callback to be of type Array<T>
What the multiple prop does is it uses what I believe is called a Distributive Conditional to determine the type of both value and onChange of the Component's props like this:
interface SelectBaseProps<T extends SelectValue = string> {
options: SelectOption<T>[];
placeholder?: string;
disabled?: boolean;
className?: string;
searchable?: boolean;
}
export type SelectProps<T extends SelectValue, TMultiple extends boolean = false> = SelectBaseProps<T> &
(TMultiple extends false
? {
multiple?: TMultiple;
value: T;
onChange: (value: T) => void;
}
: {
multiple?: TMultiple;
value: T[];
onChange: (value: T[]) => void;
});
Where SelectValue just limits the values to be of type string or number for now.
I know, not the prettiest implementation but this is already after some iterations of debugging.
And then the select component itself is basically just
export default function Select<T extends SelectValue, TMultiple extends boolean = false>(...) {...}
Now, on first sight this seems to work just fine! If I use this in a test component like this:
function SelectTest() {
const [val, setVal] = useState<string>();
const options: SelectOption<string>[] = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];
return <>
<Select value={val} options={options} onChange={(x) => console.log(x)} />
{/* However, this works! */}
<Select value={val} options={options} onChange={setVal} />
</>;
}
and hover over the onChange prop, it clearly says that the prop of the onChange callback is of type string. If I change the code and make value be an array, the onChange value is also of type array. But for some reason, the type is not infered in the function passed into the callback and typescript complains that Parameter 'x' implicitly has an 'any' type.
So my question: Why is typescript not able to infer the type here, even though the function is typed correctly and can infer the type even for custom string types?
It could be related to my tsconfig configuration, so I added it in the reproduction stackblitz:
https://stackblitz.com/edit/react-ts-wuj3yu?file=Select.tsx,tsconfig.json
Take a look: https://stackblitz.com/edit/react-ts-agwgkq?file=Select.tsx,App.tsx
I have to refuse of defining so many types/props:
import React, { useState } from 'react';
export type SelectValue = string | number | Array<string> | Array<number>;
type Unwrap<T> = T extends Array<infer R> ? R : T;
export interface SelectOption<T extends SelectValue> {
label: string;
value: Unwrap<T>;
}
interface SelectProps<T extends SelectValue> {
options: SelectOption<T>[];
value: T;
onChange: (value: T) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
searchable?: boolean;
}
export function Select<T extends SelectValue>(props: SelectProps<T>) {
return <div>Dummy Select</div>;
}
...
import * as React from 'react';
import { Select } from './Select';
import './style.css';
export default function App() {
const [val, setVal] = React.useState<string>('');
const options = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
];
const [multipleVal, setMultipleVal] = React.useState<Array<string>>([]);
return (
<React.Fragment>
<Select value={val} options={options} onChange={(x) => console.log(x)} /> // x is string
{/* However, this works! */}
<Select
value={multipleVal}
options={options}
onChange={(x) => console.log(x)} // x is Array<string>
/>
</React.Fragment>
);
}
I'm looking to extend the typescript definition for react-select such that I can pass custom ...args in Storybook to the component.
Select.data
export interface FishOption {
readonly value: string;
readonly label: string;
readonly isFixed?: boolean;
readonly isDisabled?: boolean;
}
export const fishOptions: readonly FishOption[] = [
{ value: 'Maguro', label: 'Maguro' },
{ value: 'Unagi', label: 'Unagi' },
{ value: 'Shishamo', label: 'Shishamo' },
{ value: 'Toro', label: 'Toro' },
{ value: 'Iwashi', label: 'Iwashi' }
];
node_modules/react-select/dist/declarations/src/index.d.ts
export type { StateManagerProps as Props } from './useStateManager';
Select.stories.tsx
import { Story, Meta } from '#storybook/react';
import { useState } from 'react';
import Select, { Props } from 'react-select';
import { FishOption, fishOptions } from './Select.data';
export default {
component: Select,
title: 'Components/Select',
argTypes: { onChange: { action: '' } }
} as Meta;
// Extended interface.
interface ExtendedProps extends Props {
someProp: boolean;
}
// Use extended interface.
const Template: Story<ExtendedProps> = args => {
const [value, setValue] = useState<FishOption>();
const handleChange = (newValue: FishOption) => {
setValue(newValue);
};
return <Select defaultValue={value} onChange={handleChange} {...args} />;
};
export const Single = Template.bind({});
Single.args = {
someProp: true,
options: fishOptions
};
I get the following typescript error for my onChange handler:
Type '(newValue: FishOption) => void' is not assignable to type '(newValue: unknown, actionMeta: ActionMeta<unknown>) => void'.
Types of parameters 'newValue' and 'newValue' are incompatible.
Type 'unknown' is not assignable to type 'FishOption'.ts(2322)
Looking at the other properties, the other prop (defaultValue) typings is also set to unknown as well. I'm not sure if i'm importing the types wrongly or some sort of erasure? is happening.
How can I extend react-select v5 props such that I can use them in Storybook or otherwise?
Related link: To extend the react-select interface property in typescript
CodeSandbox: https://codesandbox.io/s/interesting-hypatia-2vv6p (No storybook but observe ExtendedSelect to see that the props are unknown)
I am trying to rewrite this Apollo typescript repository to javascript.
I try to learn typescript but there is one thing called 'type casting' or 'type assertion' which makes me confuse.
let me explain it with code:
//cache.tsx
import { InMemoryCache, ReactiveVar, makeVar } from "#apollo/client";
import { Todos } from "./models/Todos";
import { VisibilityFilter, VisibilityFilters } from "./models/VisibilityFilter";
export const cache...{
...
...
}
export const todosVar: ReactiveVar<Todos> = makeVar<Todos>( //how i can 'convert' this type caster value to vanilla js?
todosInitialValue
);
export const visibilityFilterVar = makeVar<VisibilityFilter>( //how i can 'convert' this type caster value to vanilla js?
VisibilityFilters.SHOW_ALL
)
other 2 files which are used by this cache.tsx are:
//todos.tsx
export interface Todo {
text: string;
completed: boolean;
id: number
}
export type Todos = Todo[];
and
VisibilityFilter.tsx
export type VisibilityFilter = {
id: string;
displayName: string;
}
export const VisibilityFilters: { [filter: string]: VisibilityFilter } = {
SHOW_ALL: {
id: "show_all",
displayName: "All"
},
SHOW_COMPLETED: {
id: "show_completed",
displayName: "Completed"
},
SHOW_ACTIVE: {
id: "show_active",
displayName: "Active"
}
}
How can I avoid typescript type checking in this situation and more important, how can I use ReactiveVar and makeVar imports properly?
Just remove the generic like this:
From:
makeVar<Todos>(
To:
makeVar(
Change this:
export const todosVar: ReactiveVar<Todos> = makeVar<Todos>( //how i can 'convert' this type caster value to vanilla js?
todosInitialValue
);
export const visibilityFilterVar = makeVar<VisibilityFilter>( //how i can 'convert' this type caster value to vanilla js?
VisibilityFilters.SHOW_ALL
)
To this:
export const todosVar = makeVar(todosInitialValue);
export const visibilityFilterVar = makeVar(VisibilityFilters.SHOW_ALL);
How to avoid typescript checking - replace your extensions from .ts and .tsx to .js and .jsx respectively.
I am trying to write a React component for HTML heading tags (h1, h2, h3, etc.), where the heading level is specified via a prop.
I tried to do it like this:
<h{this.props.level}>Hello</h{this.props.level}>
And I expected output like:
<h1>Hello</h1>
But this is not working.
Is there any way to do this?
No way to do that in-place, just put it in a variable (with first letter capitalised):
const CustomTag = `h${this.props.level}`;
<CustomTag>Hello</CustomTag>
If you're using TypeScript, you'll have seen an error like this:
Type '{ children: string; }' has no properties in common with type 'IntrinsicAttributes'.ts(2559)
TypeScript does not know that CustomTag is a valid HTML tag name and throws an unhelpful error.
To fix, cast CustomTag as keyof JSX.IntrinsicElements!
// var name must start with a capital letter
const CustomTag = `h${this.props.level}` as keyof JSX.IntrinsicElements;
<CustomTag>Hello</CustomTag>
For completeness, if you want to use a dynamic name, you can also directly call React.createElement instead of using JSX:
React.createElement(`h${this.props.level}`, null, 'Hello')
This avoids having to create a new variable or component.
With props:
React.createElement(
`h${this.props.level}`,
{
foo: 'bar',
},
'Hello'
)
From the docs:
Create and return a new React element of the given type. The type argument can be either a tag name string (such as 'div' or 'span'), or a React component type (a class or a function).
Code written with JSX will be converted to use React.createElement(). You will not typically invoke React.createElement() directly if you are using JSX. See React Without JSX to learn more.
All the other answers are working fine but I would add some extra, because by doing this:
It is a bit safer. Even if your type-checking is failing you still
return a proper component.
It is more declarative. Anybody by looking at this component can see what it could return.
Its is more flexible for example instead of 'h1', 'h2', ... for type of your Heading you can have some other abstract concepts 'sm', 'lg' or 'primary', 'secondary'
The Heading component:
import React from 'react';
const elements = {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
h5: 'h5',
h6: 'h6',
};
function Heading({ type, children, ...props }) {
return React.createElement(
elements[type] || elements.h1,
props,
children
);
}
Heading.defaultProps = {
type: 'h1',
};
export default Heading;
Which you can use it like
<Heading type="h1">Some Heading</Heading>
or you can have a different abstract concept, for example you can define a size props like:
import React from 'react';
const elements = {
xl: 'h1',
lg: 'h2',
rg: 'h3',
sm: 'h4',
xs: 'h5',
xxs: 'h6',
};
function Heading({ size, children }) {
return React.createElement(
elements[size] || elements.rg,
props,
children
);
}
Heading.defaultProps = {
size: 'rg',
};
export default Heading;
Which you can use it like
<Heading size="sm">Some Heading</Heading>
In the instance of dynamic headings (h1, h2...), a component could return React.createElement (mentioned above by Felix) like so.
const Heading = ({level, children, ...props}) => {
return React.createElement('h'.concat(level), props , children)
}
For composability, both props and children are passed.
See Example
This is how I set it up for my project.
TypographyType.ts
import { HTMLAttributes } from 'react';
export type TagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
export type HeadingType = HTMLAttributes<HTMLHeadingElement>;
export type ParagraphType = HTMLAttributes<HTMLParagraphElement>;
export type SpanType = HTMLAttributes<HTMLSpanElement>;
export type TypographyProps = (HeadingType | ParagraphType | SpanType) & {
variant?:
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'body1'
| 'body2'
| 'subtitle1'
| 'subtitle2'
| 'caption'
| 'overline'
| 'button';
};
Typography.tsx
import { FC } from 'react';
import cn from 'classnames';
import { typography } from '#/theme';
import { TagType, TypographyProps } from './TypographyType';
const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
const paragraphs = ['body1', 'body2', 'subtitle1', 'subtitle2'];
const spans = ['button', 'caption', 'overline'];
const Typography: FC<TypographyProps> = ({
children,
variant = 'body1',
className,
...props
}) => {
const { variants } = typography;
const Tag = cn({
[`${variant}`]: headings.includes(variant),
[`p`]: paragraphs.includes(variant),
[`span`]: spans.includes(variant)
}) as TagType;
return (
<Tag
{...props}
className={cn(
{
[`${variants[variant]}`]: variant,
},
className
)}
>
{children}
</Tag>
);
};
export default Typography;
You can give this a try. I implement like this.
import { memo, ReactNode } from "react";
import cx from "classnames";
import classes from "./Title.module.scss";
export interface TitleProps {
children?: ReactNode;
className?: string;
text?: string;
variant: Sizes;
}
type Sizes = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const Title = ({
className,
variant = "h1",
text,
children,
}: TitleProps): JSX.Element => {
const Tag = `${variant}` as keyof JSX.IntrinsicElements;
return (
<Tag
className={cx(`${classes.title} ${classes[variant]}`, {
[`${className}`]: className,
})}
>
{text || children}
</Tag>
);
};
export default memo(Title);
Generalising robstarbuck's answer you can create a completely dynamic tag component like this:
const Tag = ({ tagName, children, ...props }) => (
React.createElement(tagName, props , children)
)
which you can use like:
const App = ({ myTagName = 'h1' }) => {
return (
<Tag tagName={myTagName} className="foo">
Hello Tag!
</Tag>
)
}
//for Typescript
interface ComponentProps {
containerTag: keyof JSX.IntrinsicElements;
}
export const Component = ({ containerTag: CustomTag }: ComponentProps) => {
return <CustomTag>Hello</CustomTag>;
}