skip to Main Content

Let’s say we have a Button Component that takes props variant: 'primary'|'secondary'

<Button variant='primary' on:click={()=>console.log('hello')}> click </Button>

I want to create a PrimaryButton Component that has all props and actions from Button Component but override the props with default values. I also want to do this without creating a new svelte file.

I was able to get it almost working with the following code

class PrimaryButton extends Button {
  constructor(options: ComponentConstructorOptions<Omit<ComponentProps<Button>, 'variant'>>) {
     super({...options, props: {variant: 'orange', ...options.props}})
  }
}
const PrimaryButton = extendComponent<Button>(Button, {variant: 'orange'})
<PrimaryButton on:click={()=>console.log('yay it works')}>click</PrimaryButton>

Above code gives typescript error if Button Component have other required props that I am not setting a default to.

  1. How do I fix my code to make it also work without giving defaults to all required props?

  2. How do I create a generic function that does the above so I can use it like the following with correct types.

const PrimaryButton = extendComponent(Button, {variant:'orange'})

I got close but prop types are not working

export function extendComponent<T extends SvelteComponent>(
  Comp: ComponentType,
  props: Partial<ComponentProps<T>> = {}
) {
  return class ExtendedComponent extends Comp {
    constructor(options: ComponentConstructorOptions<Omit<ComponentProps<T>, keyof typeof props>>) {
      super({...options, props: {...props, ...options.props}})
    }
  } as unknown as ComponentType<SvelteComponent<Partial<ComponentProps<T>>>>
}
const PrimaryButton = extendComponent<Button>(Button, {variant: 'orange'})
  1. Is it possible to give default on:click during runtime like how I am doing with props?

Thanks!

2

Answers


  1. To implement a generic function to extend a Svelte component with default props without creating a new file:

    1. Define a generic function called extendComponent() that takes two parameters:
      • The component to extend.
      • An object of default props.
    2. The function should return a new component type that extends the original component and overrides the default props with the values specified in the object.
    3. To use the extendComponent() function, simply pass it the component to extend and an object of default props.
    4. You can then use the extended component in your code like any other component.

    Here’s how to implement:

    1. Define the extendComponent() function:
    export function extendComponent<T extends SvelteComponent>(
      Comp: ComponentType,
      props: Partial<ComponentProps<T>> = {}
    ) {
      return class ExtendedComponent extends Comp {
        constructor(options: ComponentConstructorOptions<Omit<ComponentProps<T>, keyof typeof props>>) {
          super({...options, props: {...props, ...options.props}})
        }
      } as unknown as ComponentType<SvelteComponent<Partial<ComponentProps<T>>>>
    }
    
    1. Use the extendComponent() function to create a new component type:
    const PrimaryButton = extendComponent(Button, {variant: 'orange'})
    
    1. Use the extended component in your code:
    <PrimaryButton onClick={() => console.log('hello')} />
    

    Note: It is not possible to give default on:click handlers during runtime.

    Login or Signup to reply.
  2. You need another generic type argument for the props, otherwise the type will always just be Partial<ComponentProps<T>> which has no key information to be extracted.

    Also, the type of the slots should be extracted, so that information (along with what slot properties are passed in) is not lost. Interestingly there is no existing helper type like ComponentProps for that, but it is completely analogous.

    The full function could look like this:

    import type {
        ComponentConstructorOptions, ComponentEvents, ComponentProps,
        ComponentType, SvelteComponent
    } from 'svelte';
    
    type PartialProps<T extends ComponentType> =
        T extends ComponentType<infer C> ? Partial<ComponentProps<C>> : never;
    
    type ComponentSlots<C extends SvelteComponent> =
        C extends SvelteComponent<infer Props, infer Events, infer Slots> ? Slots : never;
    
    export function extendComponent<
        T extends ComponentType,
        PassedProps extends PartialProps<T> // <-- important bit
    >(
        Component: T,
        props: PassedProps, // <-- important bit
    ) {
        type C = T extends ComponentType<infer C> ? C : never;
        type RestProps = Omit<ComponentProps<C>, keyof PassedProps>;
    
        const constructor = function (options: ComponentConstructorOptions<RestProps>) {
            return new Component({ ...options, props: { ...props, ...options.props } as any })
        }
    
        return constructor as any as ComponentType<
            SvelteComponent<RestProps, ComponentEvents<C>, ComponentSlots<C>>
        >;
    }
    

    (Note that this code only works in CSR, in SSR the component provides a render function.)

    There are is no official way to pass events to the constructor. You could either use internals (which can change at any point) or attach them after constructing the instance using $on.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search