svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Option to set slots when create component instance

Open creaven opened this issue 6 years ago • 12 comments
trafficstars

In svelte 2 it was possible to pass slots option when creating new component:

new Component({
      target: element,
      slots: { slot_name1: element1, slot_name2: element2, ... },
});

In svelte 3 slots options seems doesn't work. And there is no way to set slots after component instance is created.

This is needed to properly integrate svelte components that using slots into other frameworks.

creaven avatar Apr 27 '19 12:04 creaven

workaround with private apis is to do something like this:

import { detach, insert, noop } from 'svelte/internal';

function createSlots(slots) {
    const svelteSlots = {};

    for (const slotName in slots) {
        svelteSlots[slotName] = [createSlotFn(slots[slotName])];
    }

    function createSlotFn(element) {
        return function() {
            return {
                c: noop,

                m: function mount(target, anchor) {
                    insert(target, element, anchor);
                },

                d: function destroy(detaching) {
                    if (detaching) {
                        detach(element);
                    }
                },

                l: noop,
            };
        }
    }
    return svelteSlots;
}

new Component({
      target: element,
      props: {
          $$slots: createSlots({ slot_name1: element1, slot_name2: element2, ... }),
          $$scope: {},
     },
});

it seems works for me

creaven avatar May 01 '19 17:05 creaven

@creaven thanks for this workaround. Saved me a headache converting a v2 modal component that relied on passing in slots to v3!

rob-balfre avatar Jun 25 '19 00:06 rob-balfre

@creaven your solution works like a charm! Now the challenge I'm facing is how to pass another component into the slot? For example, to test components that are meant to be used as:

<Dropdown>
  <DropdownTrigger />
  <DropdownList />
</Dropdown>

Where the Dropdown component just renders what's passed into the default slot, but then it also creates context, and child components interact with such context, so no possibility to fully test the whole behavior when testing them in isolation. Any suggestions?

alejandroiglesias avatar Jan 27 '21 07:01 alejandroiglesias

mark.

cbbfcd avatar Apr 26 '21 09:04 cbbfcd

also can like this:

export function createSlots(slots) {
  const svelteSlots = {}

  for (const slotName in slots) {
    svelteSlots[slotName] = [createSlotFn(slots[slotName])]
  }

  function createSlotFn([ele, props = {}]) {
    if (is_function(ele) && Object.getPrototypeOf(ele) === SvelteComponent) {
      const component: any = new ele({})
      return function () {
        return {
          c() {
            create_component(component.$$.fragment)
            component.$set(props)
          },
          m(target, anchor) {
            mount_component(component, target, anchor, null)
          },
          d(detaching) {
            destroy_component(component, detaching)
          },
          l: noop,
        }
      }
    }
    else {
      return function () {
        return {
          c: noop,
          m: function mount(target, anchor) {
            insert(target, ele, anchor)
          },
          d: function destroy(detaching) {
            if (detaching) {
              detach(ele)
            }
          },
          l: noop,
        }
      }
    }
  }
  return svelteSlots
}

then you can use like this:

const { container } = render(Row, {
    props: {
      gutter: 20,
      $$slots: createSlots({ default: [Col, { span: 12 }] }),
      $$scope: {},
    }
  })

it works for me, but i still wait the pr be merged.

cbbfcd avatar Apr 28 '21 16:04 cbbfcd

Will this feature be supported?

hua1995116 avatar Jun 22 '21 08:06 hua1995116

Hi guys

I am confused.

How to give one (or several) components (with props) to another component's slots programatically?

I tried @cbbfcd ( https://github.com/sveltejs/svelte/issues/2588#issuecomment-828578980 ) suggestion.

import Book from './components/Book.svelte';
import Bubble from './components/Bubble.svelte';

//const root = ...
const slot = [Book, { color: 'green' }];
const bubble = new Bubble({
  target: root,
  props: {
    $$slots: createSlots({ default: slot }),
    $$scope: {},
  },
});

But it fails and I updated it like so:

// from:
if (is_function(ele) && Object.getPrototypeOf(ele) === SvelteComponent) {
// to: 
if (is_function(ele) && ele.prototype instanceof SvelteComponent) {

the complete code below:

import {
  create_component,
  destroy_component,
  detach,
  insert,
  is_function,
  mount_component,
  noop,
  SvelteComponent
} from 'svelte/internal';

export const createSlots = (slots) => {
  const svelteSlots = {}

  for (const slotName in slots) {
    svelteSlots[slotName] = [createSlotFn(slots[slotName])]
  }

  function createSlotFn([ele, props = {}]) {
    if (is_function(ele) && ele.prototype instanceof SvelteComponent) {
      const component: any = new ele({})
      return function () {
        return {
          c() {
            create_component(component.$$.fragment)
            component.$set(props)
          },
          m(target, anchor) {
            mount_component(component, target, anchor, null)
          },
          d(detaching) {
            destroy_component(component, detaching)
          },
          l: noop,
        }
      }
    }
    else {
      return function () {
        return {
          c: noop,
          m: function mount(target, anchor) {
            insert(target, ele, anchor)
          },
          d: function destroy(detaching) {
            if (detaching) {
              detach(ele)
            }
          },
          l: noop,
        }
      }
    }
  }
  return svelteSlots
};

However now, it fails like so: Screen Shot 2022-05-23 at 13 00 52

The responsible line is:

// at createSlotFn (tools.ts:24:30)
const component: any = new ele({})

Any idea what I am doing wrong?

Micka33 avatar May 22 '22 17:05 Micka33

And just in case it is relevant this is how I setup svelte in webpack.

        {
          test: /\.(html|svelte)$/,
          use: {
            loader: 'svelte-loader',
            options: {
              emitCss: true,
              preprocess: sveltePreprocess({
                postcss: true,
                typescript: true,
              }),
              compilerOptions: {
                dev: !env.production,
                generate: 'dom',
              }
            },
          },
        },

Micka33 avatar May 23 '22 12:05 Micka33

@cbbfcd I partially updated you code like this to make it work.
I have no idea if this is good, suggestions are welcome.

import {
  destroy_component,
  detach,
  insert,
  is_function,
  mount_component,
  noop,
  SvelteComponent,
} from 'svelte/internal';

export const createSlots = (slots) => {
  const svelteSlots = {}

  for (const slotName in slots) {
    svelteSlots[slotName] = [createSlotFn(slots[slotName])]
  }

  function createSlotFn([ele, props = {}]) {
    if (is_function(ele) && ele.prototype instanceof SvelteComponent) {
      let component
      return function () {
        return {
          c: noop,
          m(target, anchor) {
            component = new ele({ target, props })
            mount_component(component, target, anchor, null)
          },
          d(detaching) {
            destroy_component(component, detaching)
          },
          l: noop,
        }
      }
    }
    else {
      return function () {
        return {
          c: noop,
          m: function mount(target, anchor) {
            insert(target, ele, anchor)
          },
          d: function destroy(detaching) {
            if (detaching) {
              detach(ele)
            }
          },
          l: noop,
        }
      }
    }
  }
  return svelteSlots
};

Micka33 avatar May 23 '22 12:05 Micka33

Would love to see this feature. Server side rendered Svelte does have support for Slots so I think it makes sense to have it for the client too. It's not an easy problem though

An issue I'm seeing is that if you update the HTML of the slot, and you have for example an input text field with some text in it, it would reset the field to blank on update.

For LiveSvelte (a Phoenix LiveView integration), here's how I did it. It's still a bit buggy as I'm doing some hacks to effectively update the slot data, and it does not solve the mentioned issue. https://github.com/woutdp/live_svelte/blob/master/assets/js/live_svelte/hooks.js#L15-L43

woutdp avatar Mar 14 '23 06:03 woutdp

Has anyone figured out yet how to do this in svelte 4?

Error: Cannot find module 'svelte/internal' or its corresponding type declarations.
import { detach, insert, noop } from 'svelte/internal';

codegain avatar Jun 28 '23 14:06 codegain

They are still available, we "only" removed the type definitions to discourage its use - these internal methods will likely all change in Svelte 5

dummdidumm avatar Jun 28 '23 15:06 dummdidumm

Is there appetite to resolve this in Svelte 5? Being able to programmatically identify elements as slots is very useful for rendering user-editable content programmatically, e.g. from a CMS, in a Svelte component.

cd-slash avatar Mar 13 '24 16:03 cd-slash

That is (already) possible in Svelte 5 with snippets since they can be passed as any other prop. Creating them programmatically is a bit roundabout, but possible.

brunnerh avatar Mar 13 '24 17:03 brunnerh

I didn't appreciate the power of snippets and how much they change the game vs. slots until I looked into them to solve this problem. They make it dramatically easier and completely solve my underlying problem. Thanks @brunnerh for the tip!

In particular, this is what solved the problem for me - being able to nest snippets, where I couldn't nest slots (hence trying to do something similar by setting them programmatically):

{#snippet theme()}
	<div
		style="
		position: absolute;
		left: 0px;
		top: 0;
		width: 100%;
		height: 100%;
		z-index: -99;
		"
	>
		<div style="width: 2400px; height: 500px; background: white; font-size: 240px; color: black;">
			{@render layout()}
		</div>
	</div>
{/snippet}

This could of course extend further so there's content nested inside the layout snippet, etc. It's snippets all the way down...

cd-slash avatar Mar 13 '24 21:03 cd-slash