-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
feat: attachments #15000
base: main
Are you sure you want to change the base?
feat: attachments #15000
Conversation
🦋 Changeset detectedLatest commit: 6402161 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
preview: https://svelte-dev-git-preview-svelte-15000-svelte.vercel.app/ this is an automated message |
|
Would something like this work as well? <script>
import { createAttachmentKey } from 'svelte/attachments';
const stuff = {
class: 'cool-button',
onclick: () => console.log('clicked'),
[createAttachmentKey()]: (node) => console.log(`I am one attachment`)
};
const otherStuff = {
[createAttachmentKey()]: (node) => console.log('I am another attachment')
}
</script>
<button {...stuff} {...otherStuff}>hello</button> Where the result on mount would be:
|
Personally I would prefer a createAttachment like createSnippet. Just something to consider for the team |
nice! 👍 I wonder if it would be more flexible for composition if the syntax can work with named props. programmatically: <script>
// reverse logic instead of symbol-ed key, a symbol-ed function wrapper
import { createAttachment } from 'svelte/attachments';
const stuff = {
class: 'cool-button',
onclick: () => console.log('clicked'),
showAlert: createAttachment((node) => alert(`I am a ${node.nodeName}`)),
logger: createAttachment((node) => console.log(`I am a ${node.nodeName}`)),
};
</script>
<button {...stuff}>hello</button> directly on components: <Button
class="cool-button"
onclick={() => console.log('clicked')}
showAlert={@attach (node) => alert(`I am a ${node.nodeName}`)}
logger={@attach (node) => console.log(`I am a ${node.nodeName}`)}
>
hello
</Button> and spread in which case at runtime the prop values can be checked for a special attach symbol (the prop key names are irrelevant) <script>
let { children, ...props } = $props();
</script>
<button {...props}>{@render children?.()}</button> or explicitly declare props, for further composition (and it would be nice for TypeScript declarations): <script>
import AnotherComponent from './AnotherComponent.svelte';
let { children, showAlert, logger } = $props();
</script>
<button {@attach showAlert} {@attach logger}>{@render children?.()}</button>
<AnotherComponent logger={@attach logger} /> And with either syntax, one could also just pass in a prop as an "attachable" function without <AnotherComponent {logger} myAction={(node) => { /* do something */ } /> <!-- AnotherComponent.svelte -->
<script>
let { logger, myAction } = $props();
</script>
<input {@attach logger} {@attach myAction}> |
Could svelte have a set of constant symbols (assuming we're using the Symbol API)? Could also allow for updating the transition directives. Something like: <script>
import { ATTACHMENT_SYMBOL, TRANSITION_IN_SYMBOL } from "svelte/symbols";
import { fade } from "svelte/transition";
const stuff = {
[ATTACHMENT_SYMBOL]: (node) => console.log("hello world"),
[TRANSITION_IN_SYMBOL]: (node) => fade(node, { duration: 100 }),
};
</script>
<button {...stuff}>hello</button> |
The purpose of having a function that returns symbols - rather than using a single symbol - is that it lets you have multiple attachments on a single element/component without them clobbering one another. |
The current rub with transitions is their css and/or tick methods that apply to style but if transitions were just attachments that modified the style attribute of node then they would just be attachments too... |
Actions can already do this already, the advantage of transitions is to do this outside the main thread |
One of the advantages of the special syntax of actions was the fact that it generated shakable tree code Attachments do not seem to have this advantage since every element needs to look for properties with the special symbol for special behavior |
If I understand correctly, it is not possible to extract an attachment from the props and consequently it is also not possible to prevent an attachment from being passed to an element with spread props, using an attachment on a component is basically a redirect all |
True, I'm curious about the waapi usage |
I'd be curious about the intention of this, cause intuitively I would assume using the function would override any previous definitions the same way standard merging of objects would. Allowing multiple of what at face value feels like the same key feels like it'll trip people up. <script>
import { ATTACHMENT_SYMBOL } from "svelte/symbols";
import { sequence } from "svelte/attachments";
const attachmentA = (node) => console.log("first attachment");
const attachmentA = (node) => console.log("second attachment");
const stuff = {
[ATTACHMENT_SYMBOL]: sequence(attachmentA, attachmentB),
};
</script>
<button {...stuff}>hello</button> |
You're just describing normal props! The <MyComponent {@attach anonymousAttachment} named={namedAttachment} /> <script>
let { named, ...props } = $props();
</script>
<div {@attach named} {...props} />
I don't follow? The only treeshaking that happens, happens in SSR mode — i.e.
It's deliberate that if you use
Most of the time you're not interacting with the 'key', that's an advanced use case. You're just attaching stuff: <div {attach foo()} {@attach bar()} {@attach etc()}>...</div> One possibility for making that more concise is to allow a sequence... <div {attach foo(), bar(), etc()}>...</div> ...but I don't know if that's a good idea. |
Love the proposal and how it simplified actions, specially the handler having a single parameter, which will not only encourage but force writing more composable attachments via HOFs. export const debounce = (cb: ()=>void)=>(ms: number)=>(element: HTMLElement)=>{
// implementation intentionally left blank
} <script lang="ts">
const debounced_alert = debounce(()=>alert("You type too slow"));
</script>
<textarea {@attach debounced_alert(2000)}></textarea> Personally I would prefer a block syntax rather than the PR one. <!--Applies both attachments to input and textarea-->
{#attachment debounce(()=>alert("You type too slow"))(2000), debounce(()=>alert("Server is still waiting for input"))(3000)}
<input type="text"/>
<textarea></textarea>
{/attachment} My reasons to prefer a block are:
|
I like this, my only concern is the similarity in syntax between this and logic tags. It may make new developers think that something like this is valid Svelte: <div {@const ...}> Or may make them try to do something like this: <element>
{@attach ...}
</element> |
I love it! |
Great iteration on actions, I love it! Came here to ask how I could determine if an object property was an attachment, but looking at the source, it looks like
I think it would be really intuitive if attachments could return an async cleanup function with the element/component being removed once the cleanup function (and all nested cleanup functions) settle/resolve! |
Which is aligned with HTML spec where attributes on element can only be specified one time, no? |
It's not an attribute, which is also why I prefer I don't think the just one call option is a good idea if that entails there being a single key. Symbols are nice because they do not conflict if new ones are created for every instance. With a single key it is easy to accidentally clobber attachments when merging props from different sources. |
I think about these attachments as positional entries that apply behaviour to the element in sequence, much like items in an array: <div
class="modal"
[trapfocus(), draggable({...}), fly.in({...}), fade.out({...}), ...otherAttachments]
>
<!-- ... -->
</div> |
That's why and what I'm fighting for. It should be (looks like) one attribute. And who care about forward compatibility, it takes months (even years) to standardize a new attribute in the specs, which let plenty of time to make non backward changes for any library out there. Even for real thing like Also, having an attribute on a DOM node in a Svelte template does not mean this attribute will end up on the final node, as Svelte compiler will recognize it and do some things with it.
Which fits really well with a single attribute syntax. <div attach={[trapfocus(), draggable({...})]} > It sounds easier to me from all perspective: Developer Experience, highlighter tools, ... |
I think @bcharbonnier has a good point: Even if today someone presented the proposal for the "attach" HTML attribute, it would easily take one year for it to be approved, plus some extra time until the attribute is available in the wild. That should be plenty of time to make a new major Svelte version. But let's say we take prevision further: <div @attach={[trapfocus(), draggable({...})]} > I don't think the standards will ever define an attribute that starts with "@". BTW, I support this for the typing perspective. I'm ok with this or the alternate syntax. Syntax for me is not an issue. |
Ahh I completely forgot about the index signature type limitations, that's very unfortunate 😕 I think there's probably still a workable solution for a single unique For component attachments, it would look something like: <script lang="ts">
import { Attachments, type AttachmentFunction, attach } from 'svelte/attachments';
let {
// Can support extracting just the attachments to a single variable via the `Attachments` key if desired
// [Attachments]: attachments = [],
...props,
}: {
[Attachments]?: AttachmentFunction[]
} = $props();
</script>;
<input type="text" {...props} />
<!-- or for the destructured case -->
<input type="text" attach(...attachments) /> I think the biggest drawback of doing this is it becomes very easy to unintentionally override all the attachments when spreading two objects together. To prevent this I'd probably suggest some special handling for how attachments are merged compared to normal properties on components/elements, and maybe even provide a Something like: <script lang="ts">
import { Attachments, type AttachmentFunction } from 'svelte/attachments';
import { autoSelect, tooltip } from './attachments.js';
import { mergeProps } from 'svelte';
const baseProps = {
[Attachments]: [autoSelect],
};
const otherProps = {
[Attachments]: [tooltip('Enter your name')],
};
// ⚠️ Potential footgun case - `Attachments` is overwritten by otherProps (normal JS behaviour)
const spreadProps = { ...baseProps, ...otherProps };
// Manually merging attachments by spreading inside $$Attachments
const manualMergeProps = {
...baseProps,
...otherProps,
[Attachments]: [...baseProps[Attachments], ...otherProps[Attachments]]
};
// Maybe - Introduce mergeProps convenience utility that combines attachments (and potentially merge other things like events/classes??)
const mergedProps = mergeProps(baseProps, otherProps);
</script>
<!-- Spreading baseProps and otherProps separately, both arrays attachments should be applied -->
<input type="text" {...baseProps} {...otherProps} />
<!-- ⚠️ Attachments will be overwritten, only tooltip from otherProps will remain -->
<input type="text" {...{ ...baseProps, ...otherProps }} />
<!-- Calling inline attach() will also merge with the other attachments -->
<input type="text" {...mergedProps} attach(tooltip("Now there's two of us!")) /> |
Crazy thought:
Too much? Not all ideas need to be golden. 😄 Spare my life if this is madness. This would only apply for the attribuite-like syntax, which I like because it is typed normally. |
just fyi, it's just a usage example, don't want to open up another discussion on this pr. I'm @huntabyte and others who thought it might be useful to have named props and destructure without caring what's what, With the new All callbacks whether just functions, or functions as parts of arrays or objects can be marked for attachment via NOTE: |
What if instead of checking the prop key to determine if it's an attachment, we check the value instead? We could check if the value is a typeof value === 'function' && value.$$svelte_directive_type === 'attachment' Pros:
Cons:
I kinda think the pros outweigh the cons, what do you guys think? |
I think this discussion thread has gone on for too long, because we're starting to have the same conversations on repeat :) |
I apologize, I didn't see this suggestion. I must have missed it 😞 |
I agree, it's become the most commented PR on this repo in less than two weeks... |
Yeah, I was hoping to discuss more about the attachment and transitions/animations apis but it seems others cared more about syntax, and on that I'm fine with any and leave it to svelte team and trust in y'all |
I'm loving this but I'm a bit confused by the name... can you clarify the semantics behind the choice of |
With attachments, you're attaching a function to the lifecycle of an element. The function can use the element while it is mounted to the DOM, and can perform cleanup operations when the element is destroyed. |
The term "attachments" for this is okay, I guess, but when you think about it, this feature is also about to "decorate" or extend an element with functionality — which, in a sense, is very similar to how decorators extend a class or its elements with functionality. 😎 So, for the sake of familiarity — even though I know some despise them! — why not draw some inspiration from how decorators are invoked when it comes to the syntax? Given that this functionality may be very fundamental in Svelte and that Svelte may in the future base transitions on it, I think it deserves to have a syntax that is as short as possible. So instead of this: <button {@attach tooltip('Hello')}>
Hover me
</button>
// Alternative syntax
<canvas
class="cool"
width={64}
height={64}
attach((node) => {
// ...
})
></canvas>
<input attach(autoselect) />
We could write it like this: <button @tooltip('Hello')>
Hover me
</button>
<canvas
class="cool"
width={64}
height={64}
@(node) => {
// ...
}
></canvas>
<input @autoselect />
This syntax obviously doesn’t make it possible to introduce an alternative way to spread attachments in an array form, though, like: <script>
let attachments = [
(node) => console.log('a'),
(node) => console.log('b'),
(node) => console.log('c')
];
</script>
<input attach(...attachments) /> But I don’t think that’s a good idea anyway, because it would create two ways to achieve the same thing, as you can simply use the standard spread pattern for that: <script>
let attachments = {
[Symbol()]: (node) => console.log('a'),
[Symbol()]: (node) => console.log('b'),
[Symbol()]: (node) => console.log('c'),
}
</script>
<input {...attachments} /> |
I like this more concise proposal @qwuide <div @draggable({...}) @fadeIn() @trapfocus @(node) => { console.log(node) } >
or
<div @[draggable({...}), fadeIn(), trapfocus, (node) => { console.log(node) }] >
or
<div @[...myFunctions] > |
imo syntax is very subjective. I'm fine with having the syntax initially proposed, or this proposal. I think we probably won't ever find a syntax everyone will like and agree on. |
I know the team is probably tired of syntax discussion, I don't think for rich that the syntax was what he wanted discussed, but I could dream of - <div {@ tooltip()} />
<div {@ (node) => ...}/> <script>
let props = {
[Symbol()]: (node) => ...
}
</script>
<div {...props} /> |
Personally, I am in favor of |
We've been here before. It feels to me like we already know what to do and why to do it. Suppose I have an
I'm genuinely surprised by the lack of strong opposition to the symbols and special syntax. |
The reason there isn't much opposition to the special syntax is because of how unique this feature is. While actions are a predecessor to this, the caveats they had are what make this feature so different from them. They also don't really align with the mental model of attributes, so trying to conform their syntax to fit as an attribute would lead to more confusion. Additionally, special syntax is needed for special features; for example, the |
If you find a strong argument that'll convince me, it'll be because you labored very hard to find them. I feel my previous arguments presented themselves naturally from the decisions we've already made for Svelte 5. So far we've used special syntax to denote things that are compiled/transformed. There's little to no transformation to attachments since they're just JavaScript...like event listeners. If you're talking about clarity and magic, I immediately understand the "magic" of I wrote various versions of the above post a few times over 2 weeks. I steelmanned all the arguments I could think of, and debated whether to post anything at all...given the already somewhat unproductive discourse in this thread. Because of that, this will be my last post on this thread. For each of the strongest versions of the arguments in this thread, when I start from what we have today and the experience I want to have as a library author, I end up at attributes. I suppose I have to trust the vibes. It's what brought us here together after all. |
Maybe this is an unpopular opinion, but at least for me, I haven't pushed back hard on new syntax because the project has decided that new syntax and magic is something they don't mind including. After the sveltekit discourse on "+page.svelte" the maintainers made it clear that the vibes they were going for were more in line with using naming conventions to denote new functionality instead of a more declaration or imperative approach. I'd prefer less magic but svelte* maintainers don't agree (which is fine.) So any challenges to new syntax seems unproductive. The vibes are - get on board. |
Personally I find I agree with whatever rich comes up with, he's very good at communicating his ideas and reasons why they are good and I trust him and the svelte team on the syntax and implementation but I do like to dream of alternatives, but it's not like it's set in stone. I say ship what they got and if they get enough feedback to warrant change I'm sure they will. But I see no reason to change stuff currently |
What?
This PR introduces attachments, which are essentially a more flexible and modern version of actions.
Why?
Actions are neat but they have a number of awkward characteristics and limitations:
<div use:foo={bar}>
implies some sort of equality betweenfoo
andbar
but actually meansfoo(div, bar)
. There's no way you could figure that out just by looking at itfoo
inuse:foo
has to be an identifier. You can't, for example, douse:createFoo()
— it must have been declared elsewherefoo
changes,use:foo={bar}
does not re-run. Ifbar
changes, andfoo
returned anupdate
method, that method will re-run, but otherwise (including if you use effects, which is how the docs recommend you use actions) nothing will happenWe can do much better.
How?
You can attach an attachment to an element with the
{@attach fn}
tag (which follows the existing convention used by things like{@html ...}
and{@render ...}
, wherefn
is a function that takes the element as its sole argument:This can of course be a named function, or a function returned from a named function...
...which I'd expect to be the conventional way to use attachments.
Attachments can be create programmatically and spread onto an object:
As such, they can be added to components:
Since attachments run inside an effect, they are fully reactive.
Because you can create attachments inline, you can do cool stuff like this, which is somewhat more cumbersome today.
When?
As soon as we bikeshed all the bikesheddable details.
While this is immediately useful as a better version of actions, I think the real fun will begin when we start considering this as a better version of transitions and animations as well. Today, the
in:
/out:
/transition:
directives are showing their age a bit. They're not very composable or flexible — you can't put them on components, they generally can't 'talk' to each other except in very limited ways, you can't transition multiple styles independently, you can't really use them for physics-based transitions, you can only use them on DOM elements rather than e.g. objects in a WebGL scene graph, and so on.Ideally, instead of only having the declarative approach to transitions, we'd have a layered approach that made that flexibility possible. Two things in particular are needed: a way to add per-element lifecycle functions, and an API for delaying the destruction of an effect until some work is complete (which outro transitions uniquely have the power to do today). This PR adds the first; the second is a consideration for our future selves.
Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.packages/svelte/src
, add a changeset (npx changeset
).Tests and linting
pnpm test
and lint the project withpnpm lint