sveltejs / svelte

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

Support for "High Order Component" / Wrapper / Forwarding #5106

Open j3rem1e opened 4 years ago

j3rem1e commented 4 years ago

Is your feature request related to a problem? Please describe.

I want to build a "transparent" lazy-loading component. This component should load a target component asynchronously, and when ready, delegate everything to this target.

This lazy-component should be generic: it should work for every target component. Moreover, I don't want the consumer of the component to know there is this wrapper, it should be "transparent" for it.

Today, it doesn't seem to be possible to build such "wrapper" because :

There is various issues about this :

2837 about forwardings events ;

1824 and #4295 about forwardings slots ;

I found nothing about "binding" forwardings.

4647 is about a loadable component, closed because "svelte-loadable" can implements this issue. however, it's false because it doesn't supports slots or events.

Describe the solution you'd like

An official way/api to "wrapper" transparently another component.

Describe alternatives you've considered

I am able to prototype such wrapper with internals API, however I am not able to forwards binding from parent to child :

<svelte:component this={cpn} {...slotsProps} {...$$restProps} bind:this={instance}/>

<script>
    import { get_current_component } from 'svelte/internal';

    export let provider;

    const slotsProps = {"$$slots":$$props.$$slots, "$$scope":$$props.$$scope};
    const self = get_current_component();

    let cpn;
    let instance;

    provider().then(result => cpn = result);

    $: if (instance) {
        for (let [type, listeners] of Object.entries(self.$$.callbacks)) {
            instance.$on(type, (e) => {
                listeners.forEach(l => l(e));
            });
        }
    }
</script>

How important is this feature to you?

I can't today implements this kind of component. I'd like to "wrap" heavyweight components without updating or adding loading logic to every consumer.

pospi commented 3 years ago

I just ran up against this trying to implement React-like patterns in Svelte. I'm curious what the appropriate idiom to follow here is.

In React apps, it was quite common for me to implement "controller" components which have dynamic children. Such controllers were usually for fetching external data (example: the active user ID) and passing it down to a dynamic child component. So basically, any pure view component which knows how to render something about the active user could be wrapped in one of these "controllers" to have the user ID dynamically assigned.

The naive approach I tried looks like this:

<script>
  import { getClient, query } from 'svelte-apollo'

  import { queryMyAgent } from './queries.ts'

  const client = getClient()
  const agent = query(client, { query: queryMyAgent })
</script>

{#await $agent}
  Loading...
{:then result}
  <slot contextAgent={result.data.myAgent.id}></slot>
{:catch error}
  Agent loading failed: {error}
{/await}

But the nested component does not seem to get its own contextAgent prop assigned via the slot. Which means that this boilerplate would have to be included directly in every component that wishes to inject a context agent, correct? Not ideal.

stephane-vanraes commented 3 years ago

@pospi that sounds more like a support question than an issue, which you should try using the discord chat for. But you can achieve this by using the slot let: bindings: https://svelte.dev/docs#slot_let

pospi commented 3 years ago

Thanks for that @stephane-vanraes. It looks like you need to unpack the promise to get at the real value in order for slot let: to expose the binding. But it works, and the boilerplate is only in one component now which is nice:

<script>
  import { getClient, query } from 'svelte-apollo'

  import { queryMyAgent } from './queries.ts'

  const client = getClient()
  const agent = query(client, { query: queryMyAgent })

  let loading = true
  let contextAgent
  let error

  agent.subscribe(promise => {
    promise
      /* eslint no-return-assign: 0 */
      .then(val => contextAgent = val)
      .catch(e => error = e)
      .finally(() => loading = false)
  })
</script>

{#if loading}
  Loading...
{:else if error}
  Agent loading failed: {error}
{:else}
  <slot {contextAgent}></slot>
{/if}

Should I be able to bind to the reactive props directly?

janosh commented 3 years ago

I've only started using Svelte recently but I've already run into the need to forward event bindings a few times. See e.g. codefeathers/rollup-plugin-svelte-svg#11. Would be great to know if this is on the roadmap or what's preventing it.

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

sxxov commented 2 years ago

I've written an RFC at https://github.com/sveltejs/rfcs/pull/57 in an attempt to propose a solution for this. Anyone one looking at this problem should feel free to chime in.

j3rem1e commented 7 months ago

Svelte 5 solves the slot/events forwarding. But AFAIK it's not possible to implement the "bind forwarding".