Skip to content

Commit

Permalink
WIP: Working on a new updated version to render components with asChild
Browse files Browse the repository at this point in the history
  • Loading branch information
Saartje87 committed Aug 30, 2024
1 parent 978fb90 commit 179f47a
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 11 deletions.
14 changes: 10 additions & 4 deletions src/components/form/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { forwardRef } from 'react';
import { useComponentStyles } from '../../../hooks/useComponentStyles';
import { createAsChildContainer } from '../../../lib/asChildRenderer/asChildRender';
import { Atoms, MarginAtoms, atoms } from '../../../lib/css/atoms';
import { ButtonTheme } from '../../../lib/theme/componentThemes';
import { getAtomsAndProps } from '../../../lib/utils/atom-props';
import { classnames } from '../../../lib/utils/classnames';
import { HTMLElementProps } from '../../../lib/utils/utils';
import { Spinner } from '../../feedback/Spinner';
import { Slot, createSlottable } from '../../other/Slot/Slot';
import * as styles from './Button.css';

export type ButtonProps = {
Expand All @@ -22,10 +22,16 @@ export type ButtonProps = {
endSlot?: React.ReactNode;
disabled?: boolean;
asChild?: boolean;
popovertarget?: string;
} & Omit<HTMLElementProps<HTMLButtonElement>, 'size'> &
MarginAtoms;

const Slottable = createSlottable('button');
const { Template, Slot } = createAsChildContainer({
defaultElement: 'button',
// Should have control over the props that are passed down to the elements that replace the Slot
// Black or white list?
inheritProps: ['type'],
});

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{
Expand Down Expand Up @@ -57,7 +63,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
const [atomsProps, otherProps] = getAtomsAndProps(restProps);

return (
<Slottable
<Template
ref={ref}
asChild={asChild}
disabled={disabled || loading}
Expand All @@ -68,6 +74,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{loading && <Spinner size={size} />}
<Slot>{children}</Slot>
{endSlot && <div>{endSlot}</div>}
</Slottable>
</Template>
);
});
12 changes: 6 additions & 6 deletions src/components/layout/Box/Box.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { forwardRef } from 'react';
import { createAsChildContainer } from '../../../lib/asChildRenderer/asChildRender';
import { atoms } from '../../../lib/css/atoms';
import { Atoms } from '../../../lib/css/atoms/atomTypes';
import { getAtomsAndProps } from '../../../lib/utils/atom-props';
import { classnames } from '../../../lib/utils/classnames';
import { HTMLElementProps } from '../../../lib/utils/utils';
import { createSlottable } from '../../other/Slot/Slot';

export type BoxProps = {
children?: React.ReactNode;
Expand All @@ -14,7 +14,7 @@ export type BoxProps = {
} & Atoms &
HTMLElementProps<HTMLDivElement>;

const Slottable = createSlottable('div');
const { Template, Slot } = createAsChildContainer({ defaultElement: 'div' });

export const Box = forwardRef<unknown, BoxProps>(function Box(
{ asChild, className, children, ...restProps },
Expand All @@ -23,13 +23,13 @@ export const Box = forwardRef<unknown, BoxProps>(function Box(
const [atomsProps, otherProps] = getAtomsAndProps(restProps);

return (
<Slottable
ref={ref}
<Template
ref={ref as React.RefObject<HTMLDivElement>}
asChild={asChild}
className={classnames(className, atoms(atomsProps))}
{...otherProps}
>
{children}
</Slottable>
<Slot>{children}</Slot>
</Template>
);
});
2 changes: 1 addition & 1 deletion src/components/other/Slot/Slot.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { Children, cloneElement, forwardRef, isValidElement } from 'react';
import { UknownRecord, mergeProps } from '../../../lib/react/mergeProps';
import { composeRefs } from '../../../lib/react/react';
import { UknownRecord, mergeProps } from './mergeProps';

// Subset of HTML element tags that can be used as a default element
type HTMLElementTags = 'a' | 'article' | 'button' | 'div' | 'p' | 'section' | 'span' | 'strong';
Expand Down
87 changes: 87 additions & 0 deletions src/lib/asChildRenderer/asChildRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Children, cloneElement, forwardRef, isValidElement } from 'react';
import { mergeProps } from '../react/mergeProps';
import { composeRefs } from '../react/react';

type TemplateProps = {
asChild?: boolean;
children?: React.ReactNode;
};

type CreateTemplate<T extends keyof HTMLElementTagNameMap> = {
defaultElement: T;
inheritProps?: (keyof React.HTMLProps<T>)[];
};

export function createAsChildContainer<T extends keyof HTMLElementTagNameMap>({
defaultElement,
inheritProps,

Check failure on line 17 in src/lib/asChildRenderer/asChildRender.tsx

View workflow job for this annotation

GitHub Actions / Test

'inheritProps' is defined but never used
}: CreateTemplate<T>) {
// Cast as any to avoid TS errors when using <Tag />
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Tag = defaultElement as any;

type HTMLProps = React.PropsWithoutRef<React.HTMLProps<T>>;

const Template = forwardRef<HTMLElementTagNameMap[T], TemplateProps & HTMLProps>(
function Template({ asChild, children, ...rootProps }, ref) {
// Return default element
if (!asChild) {
const tagProps = { ref, ...rootProps };

return <Tag {...tagProps}>{children}</Tag>;
}

const childrenArray = Children.toArray(children);

// Find Slot element
const slotIndex = childrenArray.findIndex((child) => {
if (!isValidElement(child)) {
return false;
}

return child.type === Slot;
});
const slot = childrenArray[slotIndex];

if (!slot) {
if (process.env.NODE_ENV === 'development') {
console.error('Template: No Slot provided');
}

return null;
}

if (!isValidElement(slot) || !isValidElement(slot.props.children)) {
return null;
}

// Render children inside Slot
const nextChildren = [...childrenArray];
// Replace Slot with children
nextChildren[slotIndex] = slot.props.children.props.children;

return cloneElement(
slot.props.children,
{
...mergeProps(rootProps, slot.props),
ref: composeRefs(ref, slot.props.children.ref),
},
nextChildren,
);
},
);

return {
Template,
Slot,
};
}

// Slot
type SlotProps = {
children: React.ReactNode;
};

const Slot: React.FC<SlotProps> = ({ children }) => {
return children;
};
File renamed without changes.

0 comments on commit 179f47a

Please sign in to comment.