Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to set slots when create component instance #2588

Closed
creaven opened this issue Apr 27, 2019 · 16 comments
Closed

Option to set slots when create component instance #2588

creaven opened this issue Apr 27, 2019 · 16 comments
Labels
feature request popular more than 20 upthumbs
Milestone

Comments

@creaven
Copy link

creaven commented Apr 27, 2019

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 creaven changed the title Option to set slots when create/render component instance Option to set slots when create component instance Apr 27, 2019
@creaven
Copy link
Author

creaven commented May 1, 2019

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

@rob-balfre
Copy link
Contributor

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

@alejandroiglesias
Copy link

@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?

@cbbfcd
Copy link

cbbfcd commented Apr 26, 2021

mark.

@cbbfcd
Copy link

cbbfcd commented Apr 28, 2021

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.

@hua1995116
Copy link

Will this feature be supported?

@pngwn pngwn added popular more than 20 upthumbs feature request and removed triage: has pr labels Jun 26, 2021
@Micka33
Copy link

Micka33 commented May 22, 2022

Hi guys

I am confused.

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

I tried @cbbfcd ( #2588 (comment) ) 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
Copy link

Micka33 commented May 23, 2022

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
Copy link

Micka33 commented May 23, 2022

@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
};

@woutdp
Copy link

woutdp commented Mar 14, 2023

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

@codegain
Copy link

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';

@dummdidumm
Copy link
Member

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

@cd-slash
Copy link

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.

@brunnerh
Copy link
Member

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.

@cd-slash
Copy link

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...

@Rich-Harris Rich-Harris added this to the 5.0 milestone Apr 2, 2024
@Rich-Harris Rich-Harris modified the milestones: 5.0, 5.x Jun 4, 2024
@Rich-Harris
Copy link
Member

This is possible in Svelte 5 with createRawSnippet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request popular more than 20 upthumbs
Projects
None yet