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

Get access to a component's own class in the script and script module ("$$self" or else) #5517

Open
pushkine opened this issue Oct 11, 2020 · 14 comments
Labels
compiler Changes relating to the compiler feature request

Comments

@pushkine
Copy link
Contributor

pushkine commented Oct 11, 2020

This is a proposal to add a $$self ( or else ) reserved keyword that resolves into a reference to a component's own class, such as to make the following output <h1>hello world !</h1>

<script>
	export let name;
</script>

{#if !name}
	<svelte:component this={$$self} name={'world'} />
{:else}
	<h1>hello {name} !</h1>
{/if}

Now that usecase is already covered by svelte:self, and even then there will always be a way to sneak that reference in to the script block or proxy it through other script modules ( see below ), yet I happen to find myself needing $$self more than any other supported "escape hatch" $$keyword, so while the argument for such keyword is definitely not compelling, I do think that it would be a nice to have

Script block workaround at component instantiation

<script>
	export let name;
	const self = arguments[0].__proto__.constructor;
</script>

{#if !name}
	<svelte:component this={self} name={'world'} />
{:else}
	<h1>hello {name} !</h1>
{/if}

For typescript users, the only way to use a component in any other way but as inlined in another component's xhtml markup is by exporting it through the script module of a proxy component, as importing from a non .svelte file is not yet supported by the vscode plugin

<!-- Utils.svelte -->
<script context="module" lang="ts">
	import CustomScrollbar from "./CustomScrollbar.svelte";

	type SvelteProps<T> = T extends Svelte2TsxComponent<infer U> ? U : never;

	export function custom_scrollbar(target, props: SvelteProps<CustomScrollbar>) {
		const instance = new CustomScrollbar({ target, props: {...props, parentNode: target} });
		return { update: (props) => instance.$set(props), destroy: () => instance.$destroy() };
	}
</script>

<!-- MyComponent.svelte -->
<script lang="ts">
	import { custom_scrollbar } from "./Utils.svelte";
</script>

<div use:custom_scrollbar={{ /** autocomplete works ! */ }}>
	...
</div>
@pngwn
Copy link
Member

pngwn commented Oct 11, 2020

What would $$self facilitate that isn’t already possible with svelte:self? You didn’t make that clear.

@pushkine
Copy link
Contributor Author

pushkine commented Oct 13, 2020

There's been 4 times
First was while using sapper pages, in order to implement native app-like transitions properly you need to keep all page components in a centralized array iterated by a #each <svelte:component this={PageComponent} /> in root _layout, so all page components roughly looked like the following

<script>
    import { getContext } from "svelte";
    import MyPageComponent from "./Component.svelte";
    getContext("page-state").push(MyPageComponent);
</script>

Not being able to push the component itself directly meant I had to make a proxy and an actual component for each page, using $$self I would have been able to do the following :

<script>
    import { getContext } from "svelte";
    getContext("page-state").push($$self);
    export let rendered = false;
</script>
{#if rendered}
    <!-- MyPageComponent content -->
{/if}

Second situation is kind of meta, I needed to add every mounted component in a centralized Set, $$self would've come in handy ( had to either write a unique hash in script or an object in each component's script module )
Third time I wanted to use a component as an action ( as described in op ) and would've liked exporting that action directly from the component's script module
Fourth time was while Iterating a #each <svelte:component />, there were cases where the component could itself be part of the iterable and I needed to provide a different logic if $$self === each_component

@mbacon-edocs
Copy link

mbacon-edocs commented May 19, 2021

@pngwn Different use case that svelte:self doesn't solve - I'm finding myself needing this now as well, dependency injection kind of scenario for me. I have a grid component which I'm making extensions for, I want to pass extensions as a property to the grid, and have the grid set on each extension the 'grid' property so those models have a reference to the grid they're attached to.. but currently it looks like every component that uses the grid component will have to use bind:this then watch for that getting set, then have the code to pass that value into the extensions, would be much nicer to have a component self serve its instance. Hope this makes sense!

Will try to clarify with some dummy code.

Current option (bind and watch the binding every time I use Grid):

// SomePage.svelte
<script>
  const ext1 = new GridExt1();
  const ext2 = new GridExt2();
  const options = new GridOptions();
  $: if (grid) {
    ext1.grid = grid;
    ext2.grid = grid;
  }
</script>
<Grid bind:this={grid} {options} extensions=[ext1, ext2]>

Like to have (just pass them in and let Grid manage itself):

// SomePage.svelte
<script>
  const ext1 = new GridExt1();
  const ext2 = new GridExt2();
  const options = new GridOptions();
</script>
<Grid {options} extensions=[ext1, ext2]>

// Grid.svelte
<script>
  export let extensions: Array<GridExt> = [];
  extensions.forEach(ext => ext.grid = $$self);
</script>

Update: Have got it working using current_component from svelte/internal but haven't tested if this still works in production

@Akolyte01
Copy link

The svelte "module context" tutorials demonstrates a pattern for using a module context to manage components that should be controlled as a group. This is accomplished using bind:this to gain a reference to a child component.
This is a useful pattern. However there are some instances where it would be very useful to bind to the component that creates the module rather than to a child component.
For example

// Popover.svelte (simplified)
<script context="module"> 
  let openPopover;
  function handleOpen() {
    openPopover.close();
    openPopover = thisPopover;
  }
</script>

<script>
  export let open = false;
  $: if (open) {
    handleOpen();
  }
  function close() {
    open = false;
  }
  let thisPopover;
</script>

{#if open}
  <some-content/>
{/if}

Currently there isn't a good way to get to set thisPopover. There are some existing workarounds but none of them are great.

  1. use get_current_component from 'svelte/internal'. As part of the internal api this is not intended for supporting this use, and could easily break in the future if changes are made to the internal implementation of svelte. An officially supported runtime function to accomplish this would also be a good solution.
  2. Add a wrapping component and then use bind:this to the component you want to control. This requires passing/binding all of the attributes intended for control through this wrapper component, which introduces a lot of duplicate code when dealing with more complex controls.
  3. Instead of passing the component to the module context, pass a callback function or object containing callback functions. This works well in simple instances, like stopping all audio players, but get convoluted very quickly when you want to allow for more complex control of a component.
  4. use bind:this with svelte:self/
{#if outer}
<svelte:self bind:this={component} outer={false}/>
{/if}

{#if !outer}
<h1>
	abc
</h1>
{/if}

This is super janky and I'm not sure you end up with a reference to the correct component.

Some potential solutions that may work well:

  1. Give access to $$self as discussed.
  2. Provide an officially supported version of get_current_component
  3. Provide a reference to the component to the onMount callback. (i.e. onMount((SvelteComponent) => void) )

@SystemParadox
Copy link

We've run into this when trying to implement type safety for component callbacks. The only way to get the type of the current component is to import itself. $$self would be so much better.

@schuetzm
Copy link

Another use case:

<!-- InfoPopup.svelte -->

<script context="module">
    export function openInfoPopup() {
        return new $$self({
            target: document.body,
        });
    }
</script>

<div class="popup">Hello World!</div>

There's currently no portable way to do this, AFAICS. <svelte:self> cannot be used in JS, and the workaround with arguments[0].__proto__.constructor requires an already existing instance.

I've found a workaround when using Webpack: __WEBPACK_DEFAULT_EXPORT__ contains the desired class; but of course this only works with Webpack (and probably isn't even guaranteed to work there, either), and when using Typescript, it's untyped and apparently it's also impossible to refer to the type of the current component.

A built-in keyword like $$self would be really nice here, maybe accompanied by a $$Self type (though the latter can be emulated using typeof $$self).

@zacharygriffee
Copy link

zacharygriffee commented Jan 8, 2023

I came here looking for an official solution to referencing the self component from the script block.

Oddly, $$self is the first argument of the instance. But results in illegal variable name validation error on server side.

For client only reference to self, this is what I do:

<script> const self = !import.meta.env.SSR && arguments[0]; console.log(self); </script>

output:

client: Pages (SvelteComponentDev)
server: false

@Eudritch
Copy link

I came here looking for an official solution to referencing the self component from the script block.

Oddly, $$self is the first argument of the instance. But results in illegal variable name validation error on server side.

For client only reference to self, this is what I do:

<script> const self = !import.meta.env.SSR && arguments[0]; console.log(self); </script>

output:

client: Pages (SvelteComponentDev) server: false

I'm curious, Is this legal? Will it change? I can't find a reference to it in the svelte docs; I'll use it anyway, but I'm hoping the svelte team doesn't patch it. Please let me know where you discovered the "arguments" variable.

@zacharygriffee
Copy link

zacharygriffee commented Feb 23, 2023

I'm curious, Is this legal? Will it change? I can't find a reference to it in the svelte docs; I'll use it anyway, but I'm hoping the svelte team doesn't patch it. Please let me know where you discovered the "arguments" variable.

I can't speak for legalities, and I certainly hope they don't patch it. In fact, I'd rather the team come up with something like $$self instead of me using hacky ways like 'arguments' or 'get_current_component'.

How I discovered the arguments is a combination of svelte/internal and the compiled version of the svelte component:

This line in svelte/internal calls the instance of the component:

? instance(component, options.props || {}, (i, ret, ...rest) => {

This is what the internal function calls on the compiled version of your svelte component:
image

The first argument is the component aka $$self we can't use/access in svelte land, second argument is the $$props which we can access in svelte land. So svelte land scripts get put into this instance function, and you can access the arguments once svelte gets compiled to vanilla javascript.

But again this only works on client side, so you got to protect it with a client/browser check. I use !import.meta.env.SSR but could wrap it in onMount arrow function if immediate action/manipulation on the self component is not needed for your use-case.

@brandonp-ais
Copy link

brandonp-ais commented Jun 19, 2023

I've run in to a use case when using the modals in @svelte-put.

I have a module scope function to open the dialog component from various places. But need to supply the svelte class component as a prop. To achieve this currently, i import the component to itself.

Example
image

@i3ene
Copy link

i3ene commented Nov 28, 2023

This is a serious issue. Having to access a reference to an instance itself through the DOM wit conditional rendering is not a good solution (as it effectively creates the component TWICE). The svelte/internal already has a function get_current_component() as @mbacon-edocs and @Akolyte01 mentioned, but it is not publicly exported. Just make this available.

@brunnerh
Copy link
Member

Components/modules can import themselves.

<!-- App.svelte -->
<script>
  import App from './App.svelte';
</script>

REPL example

@i3ene
Copy link

i3ene commented Nov 28, 2023

Components/modules can import themselves.

<!-- App.svelte -->
<script>
  import App from './App.svelte';
</script>

REPL example

But we do have the conditional rendering again. It would be nice to have something like onMount inside the script that also provides the current initialized component as a parameter to work with.
In my Use-Case I need to listen for the $destroy of a component to trigger something.

@7nik
Copy link

7nik commented Nov 30, 2023

I still don't get the reasons. Moreover, it seems here are mixed component class (which can be obtained via module self-import) and the current component instance.


conditional rendering

<script>
	export let name;
</script>

{#if !name}
	<svelte:component this={$$self} name={'world'} />
{:else}
	<h1>hello {name} !</h1>
{/if}

What's wrong with

<script>
	export let name;
</script>

<h1>hello {name || 'world'} !</h1>

or export let name = "world" or $: name ||= "world";?


Talking about @mbacon-edocs case, split the view and model+controller into separate entities and then you can pass the model+controller instance to the extensions.


Talking about popovers, I find it more convenient to use them as functions (just mount to body) or actions (when located around a certain element). And with Svelte 5 Snippets, we can even use popovers with slots via these approaches.


In my Use-Case I need to listen for the $destroy of a component to trigger something.

onDestroy?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler Changes relating to the compiler feature request
Projects
None yet
Development

No branches or pull requests