sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
77.46k stars 4.04k forks source link

Option to set slots when create component instance #2588

Open creaven opened 5 years ago

creaven commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

alejandroiglesias commented 3 years ago

@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 commented 3 years ago

mark.

cbbfcd commented 3 years ago

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 commented 3 years ago

Will this feature be supported?

Micka33 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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