Skip to content

Instantly share code, notes, and snippets.

@jakehamilton
Created October 26, 2021 00:18
Show Gist options
  • Select an option

  • Save jakehamilton/893df681c0168be9d277b1f0e671b06b to your computer and use it in GitHub Desktop.

Select an option

Save jakehamilton/893df681c0168be9d277b1f0e671b06b to your computer and use it in GitHub Desktop.
Preact Dynamic Components
import { FunctionComponent, VNode } from "preact";
import { JSXInternal } from "preact/src/jsx";
type Error<Message extends string> = { readonly __message: Message };
type At<Arr extends Array<unknown>, N extends number> = Arr extends Record<
N,
infer T
>
? T
: never;
type Replace<Arr extends Array<unknown>, T> = Arr extends [
infer X,
...infer Rest
]
? [T, ...Replace<Rest, T>]
: [];
type Zip2<
ArrX extends Array<unknown>,
ArrY extends Array<unknown>
> = ArrX extends [infer X, ...infer RestX]
? ArrY extends [infer Y, ...infer RestY]
? [[X, Y], ...Zip2<RestX, RestY>]
: Error<"Zip: Expected arrays to be the same length.">
: [];
type Zip3<
ArrX extends Array<unknown>,
ArrY extends Array<unknown>,
ArrZ extends Array<unknown>
> = ArrX extends [infer X, ...infer RestX]
? ArrY extends [infer Y, ...infer RestY]
? ArrZ extends [infer Z, ...infer RestZ]
? [[X, Y, Z], ...Zip3<RestX, RestY, RestZ>]
: Error<"Zip: Expected arrays to be the same length.">
: Error<"Zip: Expected arrays to be the same length.">
: [];
type Elements = JSXInternal.IntrinsicElements;
type ElementNames = keyof Elements;
type PropsObject = object;
type KnownElement = ElementNames | FunctionComponent<any>;
type DynamicAs = KnownElement;
type ExtendedPropsMetaShape = {
as: DynamicAs;
key: string;
props: PropsObject;
required?: boolean;
};
type ExtendedUnit = [DynamicAs, ExtendedPropsMetaShape];
type DynamicProps<As extends DynamicAs, Props extends PropsObject> = {
as?: As;
} & Props &
Omit<
As extends ElementNames
? Elements[As]
: As extends FunctionComponent<infer ComponentProps>
? ComponentProps
: Record<string, any>,
keyof Props
>;
type GetExtendedPropsMeta<Unit extends ExtendedUnit> = Unit extends [
infer As,
infer Meta
]
? Meta extends ExtendedPropsMetaShape
? As extends DynamicAs
? Meta["required"] extends true
? Record<Meta["key"], DynamicProps<As, Meta["props"]>>
: Partial<
Record<
Meta["key"],
DynamicProps<As, Meta["props"]> | undefined
>
>
: Error<"GetExtendedPropsMeta: Expected [DynamicAs, ExtendedPropsMetaShape].">
: Error<"GetExtendedPropsMeta: Expected [DynamicAs, ExtendedPropsMetaShape].">
: Error<"GetExtendedPropsMeta: Could not infer Props from [As, ExtendedPropsMetaShape].">;
type MergeExtendedPropsMeta<
Args extends Array<ExtendedUnit> = []
> = Args extends [infer Unit]
? Unit extends ExtendedUnit
? GetExtendedPropsMeta<Unit>
: Error<"MergeExtendedPropsMeta: Expected [As, ExtendedPropsMetaShape].">
: Args extends [infer Unit, ...infer Rest]
? Unit extends ExtendedUnit
? Rest extends Array<ExtendedUnit>
? GetExtendedPropsMeta<Unit> & MergeExtendedPropsMeta<Rest>
: Error<"MergeExtendedPropsMeta: Expected Array<ExtendedUnit>.">
: Error<"MergeExtendedPropsMeta: Expected [As, ExtendedPropsMetaShape].">
: {};
type MergeProps<
As extends DynamicAs,
Props extends PropsObject,
ExtendedPropsMeta extends Array<ExtendedPropsMetaShape> = [],
ExtendedAs extends Array<DynamicAs> = []
> = DynamicProps<As, Props> &
MergeExtendedPropsMeta<Zip2<ExtendedAs, ExtendedPropsMeta>>;
type DynamicComponent<
DefaultAs extends DynamicAs,
Props extends PropsObject,
ExtendedPropsMeta extends Array<ExtendedPropsMetaShape> = []
> = <
As extends DynamicAs = DefaultAs,
ExtendedAs extends Replace<ExtendedPropsMeta, DynamicAs> = Replace<
ExtendedPropsMeta,
DynamicAs
>
>(
props: MergeProps<As, Props, ExtendedPropsMeta, ExtendedAs>
) => VNode<MergeProps<As, Props, ExtendedPropsMeta, ExtendedAs>>;
// Dynamically render an element with the right props.
const Dynamic = <As extends DynamicAs>(
props: {
as: As;
} & As extends ElementNames
? As extends keyof Elements
? Elements[As]
: Error<"Dynamic: Expected As to extend keyof Elements.">
: As extends FunctionComponent<infer ComponentProps>
? ComponentProps
: Record<string, any>
): VNode<any> => {
const { as, children, ...rest } = props;
// @ts-expect-error
// TypeScript's JSX injection ensures that the JSX
// factory is included already. Importing it again
// would break the build. Instead, we tell TypeScript
// to ignore the missing import for the `h` function.
return h(props.as!, rest, children);
};
// ==============================
// DEMO
// ==============================
// Base component properties.
interface MyComponentProps {}
// Augments to allow props passed to dynamic children.
type MyExtendedPropsMeta = [
{
as: "div";
key: "ChildProps";
props: {
onHeck: () => void;
};
}
];
// Creating a dynamic component that can use a custom
// element or component via the "as" prop. This also
// supports customizing its children via the configured
// ChildProps key in MyExtendedPropsMeta.
const MyComponent: DynamicComponent<
"div",
MyComponentProps,
MyExtendedPropsMeta
> = (props) => {
const { as = "div", ChildProps } = props;
return (
<Dynamic as={as}>
<Dynamic as="div" {...ChildProps} />
</Dynamic>
);
};
// Render out our component with a custom element for
// the root and children. Notice that the types for
// props are correctly updated (eg. onClick is a
// handler for the specific element).
const a = (
<MyComponent
as="p"
onClick={() => {}}
ChildProps={{
as: "a",
onHeck: () => {},
onClick: () => {},
}}
/>
);
interface MySubComponentProps {
name: string;
}
const MySubComponent: FunctionComponent<MySubComponentProps> = ({
name = "World",
children,
}) => {
return (
<>
Hello, {name}! {children}
</>
);
};
// Render out our component using another component
// as the child element for both the root and inner
// child.
const b = (
<MyComponent
as={MySubComponent}
name="Jake"
ChildProps={{
as: MySubComponent,
onHeck: () => {},
// @FIXME(jakehamilton): This should be a type error.
name: 42,
}}
/>
);
// Render a dynamic element
const c = <Dynamic as="canvas" onClick={() => {}} />;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment