Created
October 26, 2021 00:18
-
-
Save jakehamilton/893df681c0168be9d277b1f0e671b06b to your computer and use it in GitHub Desktop.
Preact Dynamic Components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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