Replies: 25 comments
-
I found part of my answer with const Button = ({ children, icon, asChild, ...props }) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp {...props}>
<Slottable>{children}</Slottable>
{!!icon && <div>{icon}</div>}
</Comp>
)
} But it still doesn't allow for use cases like these: const Button = ({ children, icon, asChild, ...props }) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp {...props}>
<div>
<Slottable>{children}</Slottable>
{!!icon && <div>{icon}</div>}
</div>
</Comp>
)
} |
Beta Was this translation helpful? Give feedback.
-
Heya, Running into the same issue. I am trying something like this: const Button = React.forwardRef<HTMLButtonElement, IButton>(
({ prefixElement, suffixElement, asChild, children, props }, forwardedRef) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp className="button" ref={forwardedRef} {...props}>
{prefixElement && <div className="button__prefix">{prefixElement}</div>}
<Slottable>
<span className="button__base">{children}</span>
</Slottable>
{suffixElement && <div className="button__suffix">{suffixElement}</div>}
</Comp>
);
},
); Results in <span className="button__base">
<div className="button__prefix">...</div>
<a></a>
<div className="button__suffix">...</div>
</span> or ...
<span className="button__base">
<Slottable>
{children}
</Slottable>
</span>
... Results in: Both cases sadly don't work. It would be great if this would be a tiny bit more composable to allow for this edge-case. |
Beta Was this translation helpful? Give feedback.
-
Facing the same edge case here, did you find a solution or alternative @henrikwirth @fcisio? |
Beta Was this translation helpful? Give feedback.
-
I think I get it to work. Is this your use case @fcisio? https://codesandbox.io/s/nice-leavitt-qhmgmn?file=/src/App.js |
Beta Was this translation helpful? Give feedback.
-
Hi @joaom00 it does answer this use case. Perhaps it could be logic packaged in the library for easier use. |
Beta Was this translation helpful? Give feedback.
-
If the Radix team wants to support this case I'll be happy to help by raising a PR! @benoitgrelard @andy-hook I would like to know your thoughts! |
Beta Was this translation helpful? Give feedback.
-
Hey there, I'm not sure I really follow what you all are after.
|
Beta Was this translation helpful? Give feedback.
-
The use-case is to support nested children. Something like const Button = ({ children, asChild, ...props }) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp {...props}>
<div>
<Slottable>{children}</Slottable>
</div>
</Comp>
)
}
Given the example above This <Button asChild>
<a>Link</a>
</Button> Should result in <a>
<div>Link</div>
</a> |
Beta Was this translation helpful? Give feedback.
-
I see, to do this we would have to recursively parse the children, I don't know if it's a good idea, although we're already doing one level deep… I'll look into it. |
Beta Was this translation helpful? Give feedback.
-
Based on @joaom00 code, I came up with this solution: https://codesandbox.io/s/determined-leakey-73pndz?file=/src/App.js If we change the way |
Beta Was this translation helpful? Give feedback.
-
@joaom00 I think it's almost there! I noticed that the props from If you add this to your sandbox, you'll see that only <Component style={{ opacity: 0.5 }} {...props}>
<NewSlottable asChild={asChild} child={children}> |
Beta Was this translation helpful? Give feedback.
-
Need to pass the props to - const NewSlottable = ({ asChild, child, children }) => {
+ const NewSlottable = ({ asChild, child, children, ...props }) => {
return (
<>
{asChild
? React.isValidElement(child)
- ? React.cloneElement(child, undefined, children(child.props.children))
+ ? React.cloneElement(child, props, children(child.props.children))
: null
: children(child)}
</>
);
}; |
Beta Was this translation helpful? Give feedback.
-
In this way, the props will not be merged. Maybe a better solution is to change the |
Beta Was this translation helpful? Give feedback.
-
Why not just making a new props object with const Button = ({ children, icon, asChild, ...props }) => {
const Comp = asChild ? Slot : 'button'
const newProps = {
children = (
<>
<div>{children}</div>
{!!icon && <div>{icon}</div>}
</>
),
...props,
};
return (
<Comp {...newProps} />
)
} Haven't tried it, just thinking out loud. |
Beta Was this translation helpful? Give feedback.
-
Also wondering how to deal with multiple nested children with Slot main usecase is so that we can support a Button with So we can something like this.. const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, size = 'small', type = 'primary', ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
const { className, disabled, loading, icon, iconLeft, iconRight } = props
const showIcon = loading || icon
// decrecating 'showIcon' for rightIcon
const _iconLeft: React.ReactNode = icon ?? iconLeft
return (
<Comp
className={cn(buttonVariants({ type, size, disabled }), className)}
ref={ref}
type={props.htmlType}
{...props}
>
{showIcon &&
(loading ? (
<IconLoader size={size} className={cn(loadingVariants({ loading }))} />
) : _iconLeft ? (
<IconContext.Provider value={{ contextSize: size }}>{_iconLeft}</IconContext.Provider>
) : null)}
{props.children && <span className={'truncate'}>{props.children}</span>}
{iconRight && !loading && (
<IconContext.Provider value={{ contextSize: size }}>{iconRight}</IconContext.Provider>
)}
</Comp>
)
}
) Currently this results in: import Link from 'next/link'
<Button asChild><Link href="/sign-in-sso">Continue with SSO</Link></Button> <span class="truncate"><a href="/sign-in-sso">Continue with SSO asds</a></span> |
Beta Was this translation helpful? Give feedback.
-
Also have this use case. Want to build a button that can be passed asChild and a next Link. This is so that I can create links that are styled like buttons (can have icons, loading state too) but behave like links. <Comp
className={cn(
buttonVariants({
variant,
size,
color,
className,
}))}
ref={ref}
disabled={disabled || loading}
{...props}
>
{loading ? (
<LoadingSpinner />
) : (
<>
{LeftIcon && (
<LeftIcon size={iconSize} weight="bold" color="currentColor" />
)}
{props.children}
{RightIcon && (
<RightIcon size={iconSize} weight="bold" color="currentColor" />
)}
</>
)}
</Comp> |
Beta Was this translation helpful? Give feedback.
-
I'm using this in my project. You can define your import { Slot } from "@radix-ui/react-slot";
import { ReactNode, cloneElement, forwardRef, isValidElement } from "react";
import { mergeProps } from "react-aria";
/** TYPES */
export type SlotChildProps = {
asChild?: boolean;
child: ReactNode;
children: ReactNode | ((child: ReactNode) => ReactNode);
};
/** FUNCTIONS */
function getContent({ children }: SlotChildProps, arg: ReactNode) {
return typeof children === "function" ? children(arg) : children;
}
/** COMPONENTS */
export const SlotChild = forwardRef<any, SlotChildProps>((props, ref) => {
const { asChild, child, children, ...attrs } = props;
if (!isValidElement(child)) {
return asChild ? null : getContent(props, child);
}
const slot = child.type === Slot;
const childSlot = !!child.props.asChild;
return cloneElement(
child,
mergeProps(child.props, attrs, { ref }),
slot || childSlot ? (
<SlotChild
asChild={asChild}
child={child.props.children}
children={children}
/>
) : (
getContent(props, child.props.children)
),
);
}); It works quite similarly with other implementations I think, but also handles some edge cases. function Button({ asChild, children, ...attrs }) {
const Component = asChild ? Slot : "button";
return (
<Component {...attrs}>
<SlotChild asChild={asChild} child={children}>
{(child) => (
<>
<span>head</span>
<span>{child}</span>
<span>tail</span>
</>
)}
</SlotChild>
</Component>
);
}
// With direct <Slot>, though you're likely never do this
<Button asChild>
<Slot>
<a>text</a>
</Slot>
</Button>;
// With custom component that might have <Slot>
const Red = forwardRef(({ asChild, ...attrs }, ref) => {
const Component = asChild ? Slot : "div";
return <Component {...attrs} data-red ref={ref} />;
});
<Button asChild>
<Red asChild>
<a>text</a>
</Red>
</Button>;
// Use without function if not depending on child
function Button2({ asChild, children, ...attrs }) {
const Component = asChild ? Slot : "button";
return (
<Component {...attrs}>
<SlotChild asChild={asChild} child={children}>
<span>head</span>
<span>midd</span>
<span>tail</span>
</SlotChild>
</Component>
);
} |
Beta Was this translation helpful? Give feedback.
-
I had to find a solution to use asChild on a button component that was internally using a wrapper element around its contents, e.g. <button>
<span>
click me
</span>
</button> When using asChild + Slot with e.g. an anchor tag, the result should have been: <a>
<span>
click me
</span>
</a> But instead, the result was this: <span>
<a>
click me
</a>
</span> I was able to work around the issue with this construct: if (asChild && isValidElement(children)) {
return (
<Slot>
{cloneElement(
children,
children.props,
<span>
{children.props.children}
</span>,
)}
</Slot>
);
} |
Beta Was this translation helpful? Give feedback.
-
Yeah it works fine but requires a bit of VNode introspection/manipulation, especially with TS. |
Beta Was this translation helpful? Give feedback.
-
Thanks! This works perfectly for me too @mfellner |
Beta Was this translation helpful? Give feedback.
-
What i ended up doing was slightly different. I gave the Button component a ie.
which would just render a normal button element. But in the cases where i want to render another component, say an anchor or react-router-dom Link i do:
This will render an anchor with my icons and label props within it. Basic implementation:
Im not sure if this is the best way to do this, but seems to be working |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
The approach from @mfellner works for me too 👍
import React, { cloneElement, ElementType, HTMLAttributes, isValidElement, ReactElement, ReactNode } from "react";
import { Slot } from "@radix-ui/react-slot";
type NestedSlotProps = HTMLAttributes<HTMLElement> & {
component: ElementType;
render: ElementType<{ children: ReactNode }>;
children?: ReactNode;
asChild?: boolean;
};
export function NestedSlot({
component: Component,
render: Wrapper,
asChild = false,
children,
...rest
}: NestedSlotProps) {
if (asChild && isValidElement(children)) {
const element = children as ReactElement<{
children: ReactNode;
asChild?: boolean;
}>;
return (
<Slot {...rest}>
{cloneElement(
element,
element.props,
element.props.asChild ? (
<NestedSlot
component={Component}
render={Wrapper}
asChild={Boolean(element.props.asChild)}
>
{element.props.children}
</NestedSlot>
) : (
<Wrapper>{element.props.children}</Wrapper>
),
)}
</Slot>
);
}
return (
<Component {...rest}>
<Wrapper>{children}</Wrapper>
</Component>
);
} Example usage: import React, { ComponentPropsWithRef, ReactNode } from "react";
import { NestedSlot } from "./nestedSlot";
export type ButtonProps = {
asChild?: boolean;
} & React.ComponentPropsWithRef<"button">;
export const Button = (props: ButtonProps) => {
return (
<NestedSlot
component={"button"}
render={({ children }) => <span>{children}</span>}
{...props}
/>
);
}; |
Beta Was this translation helpful? Give feedback.
-
I encapsulated a nested child resolver (based on prior art in this thread) in way that I think is a fairly simple: import type { ReactElement, ReactNode } from 'react'
import { cloneElement, isValidElement } from 'react'
function isReactElementWithChildren(element: unknown): element is ReactElement<{
children?: ReactNode
}> {
return (
isValidElement(element) &&
'props' in element &&
element.props !== null &&
typeof element.props === 'object' &&
'children' in element.props
)
}
type ResolveNestedChildrenForSlotCallback = (props: {
nestedChildren?: ReactNode
}) => ReactNode
type ResolveNestedChildrenForSlotProps = {
callback?: ResolveNestedChildrenForSlotCallback
children: ReactNode
}
export function resolveNestedChildrenForSlot({
callback,
children,
}: ResolveNestedChildrenForSlotProps) {
return isReactElementWithChildren(children)
? cloneElement(
children,
children.props,
callback?.({ nestedChildren: children.props.children }) ??
children.props.children,
)
: (callback?.({ nestedChildren: children }) ?? children)
} Example implementation in a component called import { Slot } from '@radix-ui/react-slot'
import { resolveNestedChildrenForSlot } from './resolve-nested-children-for-slot.ts'
type ItemProps = {
asChild?: boolean
children: ReactNode
}
export function Item({ asChild = false, children }: ItemProps) {
const Component = asChild ? Slot : 'a'
const resolvedChildren = resolveNestedChildrenForSlot({
callback({ nestedChildren }) {
return (
<div>
<span>
{nestedChildren}
</span>
</div>
)
},
children,
})
return <Component>{resolvedChildren}</Component>
} |
Beta Was this translation helpful? Give feedback.
-
We have an internal utility for this that we use instead of https://github.com/optimizely-axiom/optiaxiom/blob/main/packages/react/src/utils/decorateChildren.ts import { cloneElement, isValidElement, type ReactNode } from "react";
export const decorateChildren = (
{ asChild, children }: { asChild?: boolean; children?: ReactNode },
decorator: (children: ReactNode) => ReactNode,
): ReactNode => {
if (asChild) {
const newElement = isValidElement(children) ? children : null;
return newElement
? cloneElement(
newElement,
undefined,
decorateChildren(
{
...newElement.props,
asChild:
newElement.props.asChild ||
(typeof newElement.type !== "string" &&
"displayName" in newElement.type &&
typeof newElement.type.displayName === "string" &&
newElement.type.displayName?.endsWith(".Slot")),
},
decorator,
),
)
: children;
} else {
return decorator(children);
}
}; here is an example usage: const Comp = asChild ? Slot : "button";
return (
<Box asChild>
<Comp>
{decorateChildren({ asChild, children }, (children) => (
<div>
{addonBefore}
{children}
{addonAfter}
</div>
))}
</Comp>
</Box>
); This will render by default: <button>
<div>
Tab trigger
</div>
</button> and when composed with a link will render: <a>
<div>
Tab trigger
</div>
</a> The only hacky thing here is using |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Question
Hi, I'm new to Radix!
I have a use case where I want to create my own
asChild
prop.Let's say a button's children are nested, then how do I properly clone the actual
children
and not the immediate nested element?For example, this would result in
<div />
instead of a<label />
Hope this is clear, thanks!
Beta Was this translation helpful? Give feedback.
All reactions