Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: attachments#15000

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Rich-Harris merged 36 commits intomainfromattachments
May 14, 2025
Merged

feat: attachments#15000

Rich-Harris merged 36 commits intomainfromattachments
May 14, 2025

Conversation

Rich-Harris
Copy link
Member

@Rich-HarrisRich-Harris commentedJan 13, 2025
edited
Loading

What?

This PR introducesattachments, 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:

  • the syntax is very weird!<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 it
  • thefoo inuse:foo has to be an identifier. You can't, for example, douse:createFoo() — it must have been declared elsewhere
  • as a corollary, you can't do 'inline actions'
  • it's not reactive. Iffoo changes,use:foo={bar} does not re-run. Ifbar changes, andfoo returned anupdate method, that methodwill re-run, but otherwise (including if you use effects, which is how thedocs recommend you use actions) nothing will happen
  • you can't use them on components
  • you can't spread them, so if you want to add both attributes and behaviours you have tojump through hoops

We 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:

<div {@attach (node) => console.log(node)}>...</div>

This can of course be a named function, or afunctionreturned from a named function...

<button {@attachtooltip('Hello')}>  Hover me</button>

...which I'd expect to be the conventional way to use attachments.

Attachments can be create programmatically and spread onto an object:

<script>conststuff= {    class:'cool-button',onclick: ()=>console.log('clicked'),    [Symbol()]: (node)=>alert(`I am a${node.nodeName}`)  };</script><button {...stuff}>hello</button>

As such, they can be added to components:

<Buttonclass="cool-button"onclick={()=>console.log('clicked')}  {@attach (node) => alert(`I am a ${node.nodeName}`)}>  hello</Button>
<script>let { children,...props }=$props();</script><button {...props}>{@renderchildren?.()}</button>

Since attachments run inside an effect, they are fully reactive.

I haven't figured out if it should be possible to return a cleanup function directly from an attachment, or if you should need to create a child effect

Because you can create attachments inline, you can docool 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, thein:/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

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC:https://github.com/sveltejs/rfcs
  • Prefix your PR title withfeat:,fix:,chore:, ordocs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code withinpackages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests withpnpm test and lint the project withpnpm lint

ConProgramming, endigo9740, SikandarJODD, satohshi, Ocean-OS, Devr-pro, xmlking, AnatoleLucet, sacrosanctic, NaviTheCoderboi, and 117 more reacted with thumbs up emojiSdju, asv7c2, cliffordkleinsr, and leninnava reacted with thumbs down emojiitsmikesharescode, jnsprnw, grischaerbe, Antonio-Bennett, levibassey, KittenzExe, hanielu, TGlide, johannesmutter, PuruVJ, and 22 more reacted with hooray emojimaietta, acutmore, itsmikesharescode, grischaerbe, Antonio-Bennett, levibassey, hanielu, PuruVJ, MomenNano, dollannn, and 31 more reacted with heart emojihuntabyte, maietta, itsmikesharescode, grischaerbe, Antonio-Bennett, mustafa0x, levibassey, hanielu, PuruVJ, MomenNano, and 26 more reacted with rocket emojiitsmikesharescode, Antonio-Bennett, wiesson, PuruVJ, hazelnutcloud, tlrib, PawnB4, chvanam, hanielu, abdetaterefe, and 7 more reacted with eyes emoji
@changeset-botchangeset-bot
Copy link

changeset-botbot commentedJan 13, 2025
edited
Loading

🦋 Changeset detected

Latest commit:8255bc6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
NameType
svelteMinor

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

@Rich-Harris
Copy link
MemberAuthor

preview:https://svelte-dev-git-preview-svelte-15000-svelte.vercel.app/

this is an automated message

@github-actionsGitHub Actions
Copy link
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@15000

@huntabyte
Copy link
Member

Would something like this work as well?

<script>import {createAttachmentKey }from'svelte/attachments';conststuff= {    class:'cool-button',onclick: ()=>console.log('clicked'),    [createAttachmentKey()]: (node)=>console.log(`I am one attachment`)  };constotherStuff= {[createAttachmentKey()]: (node)=>console.log('I am another attachment')}</script><button {...stuff} {...otherStuff}>hello</button>

Where the result on mount would be:

I am one attachmentI am another attachment
SikandarJODD and xmlking reacted with thumbs up emoji

@JonathonRP
Copy link

Personally I would prefer a createAttachment like createSnippet. Just something to consider for the team

bhupeshpr25, danielrincondev, cliffordkleinsr, and SikandarJODD reacted with thumbs up emoji

@Leonidaz
Copy link
Contributor

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 wrapperimport {createAttachment }from'svelte/attachments';conststuff= {    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:

<Buttonclass="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}>{@renderchildren?.()}</button>

or explicitly declare props, for further composition (and it would be nice for TypeScript declarations):

<script>importAnotherComponentfrom'./AnotherComponent.svelte';let { children, showAlert, logger }=$props();</script><button {@attachshowAlert} {@attachlogger}>{@renderchildren?.()}</button><AnotherComponentlogger={@attachlogger} />

And with either syntax, one could also just pass in a prop as an "attachable" function without{@attach} syntax if they're going to eventually explicitly attach it to a DOM element without spreading.

<AnotherComponent {logger}myAction={(node)=> {/* do something*/ }/>
<!-- AnotherComponent.svelte --><script>let { logger, myAction }=$props();</script><input {@attachlogger} {@attachmyAction}>
JonathonRP, JReinhold, AlbertMarashi, cofl, and SikandarJODD reacted with thumbs up emoji7nik reacted with thumbs down emoji

@dotmrjosh
Copy link

dotmrjosh commentedJan 14, 2025
edited
Loading

Could svelte have a set of constant symbols (assuming we're using theSymbol 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";conststuff= {    [ATTACHMENT_SYMBOL]: (node)=>console.log("hello world"),    [TRANSITION_IN_SYMBOL]: (node)=>fade(node, { duration:100 }),  };</script><button {...stuff}>hello</button>
JonathonRP, aypm-dev, and asahelk reacted with thumbs up emoji

@Conduitry
Copy link
Member

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.

levibassey, kasmacioma, TGlide, TheOnlyTails, SikandarJODD, huntabyte, jjones315, oliverturner, RyanYANG52, skaldebane, and 4 more reacted with thumbs up emoji

@JonathonRP
Copy link

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

@Thiagolino8
Copy link

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
In svelte 3/4 creating css transitions and in svelte 5 using the WAAPI

@Thiagolino8
Copy link

Thiagolino8 commentedJan 14, 2025
edited
Loading

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

ConProgramming, Rimevel, MichaelBrunn3r, and phi-bre reacted with thumbs up emoji

@Thiagolino8
Copy link

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

seanlail reacted with eyes emoji

@JonathonRP
Copy link

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
In svelte 3/4 creating css transitions and in svelte 5 using the WAAPI

True, I'm curious about the waapi usage

@dotmrjosh
Copy link

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.

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.
I think if you wanted to have multiple attachments through props, using a sequence function would be a lot clearer and guarantee what order attachments occur in.

<script>import {ATTACHMENT_SYMBOL }from"svelte/symbols";import {sequence }from"svelte/attachments";constattachmentA= (node)=>console.log("first attachment");constattachmentA= (node)=>console.log("second attachment");conststuff= {    [ATTACHMENT_SYMBOL]:sequence(attachmentA, attachmentB),  };</script><button {...stuff}>hello</button>
JonathonRP and levibassey reacted with thumbs up emoji

@Rich-Harris
Copy link
MemberAuthor

@huntabyte

Would something like this work as well?

try it and see :)

I wonder if it would be more flexible for composition if the syntax can work with named props.

You're just describing normal props! The{@attach ...} keyword is only useful when it's anonymous.

<MyComponent {@attachanonymousAttachment}named={namedAttachment} />
<script>let { named,...props }=$props();</script><div {@attachnamed} {...props} />

One of the advantages of the special syntax of actions was the fact that it generated shakable tree code

I don't follow? The only treeshaking that happens, happens in SSR mode — i.e.<div use:foo> doesn't result infoo being called on the server. That remains true for attachments. The additional runtime code required to support attachments is negligible.

If I understand correctly, it is not possible to extract an attachment from the props

It's deliberate that if you use{...stuff} that attachments will be included in that. If you really want to remove them it's perfectly possible, it's just an object with symbols. Create a derived that filters the symbols out if you need to, though I'm not sure why you'd want that.

Allowing multiple of what at face value feels like the same key feels like it'll trip people up.

Most of the time you're not interacting with the 'key', that's an advanced use case. You're just attaching stuff:

<div {attachfoo()} {@attachbar()} {@attachetc()}>...</div>

One possibility for making that more concise is to allow a sequence...

<div {attachfoo(),bar(),etc()}>...</div>

...but I don't know if that's a good idea.

JonathonRP, itsmikesharescode, levibassey, bennymi, hanielu, unlocomqx, hazelnutcloud, hjaber, oneezy, EdwardLemayDev, and 4 more reacted with thumbs up emoji

@kran6a
Copy link

kran6a commentedJan 14, 2025
edited
Loading

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.
i.e:

exportconstdebounce=(cb:()=>void)=>(ms:number)=>(element:HTMLElement)=>{// implementation intentionally left blank}
<scriptlang="ts">const debounced_alert=debounce(()=>alert("You type too slow"));</script><textarea {@attachdebounced_alert(2000)}></textarea>

Personally I would prefer a block syntax rather than the PR one.

<!--Applies both attachments to input and textarea-->{#attachmentdebounce(()=>alert("You type too slow"))(2000),debounce(()=>alert("Server is still waiting for input"))(3000)}    <inputtype="text"/>    <textarea></textarea>{/attachment}

My reasons to prefer a block are:

  1. It is an already known syntax
  2. Easily discoverable via intellisense when you type{# (to write any other block) and the autocomplete brings upattachment as one of the options. I don't think anybody that does not know about attachments is going to discover the PR syntax via intellisense.
  3. Blocks are easier to read when the block content is a big tree since you can see the opening and closing. This is useful when the element that has the attachment is not an input/button but a clickoutside or a keydown on a whole page section.
  4. Syntax is cleaner even if you inline the attachment configuration options as otherwise they would be on the same line as 20 tailwind classes, anid,name,data- andaria- attributes.
  5. The{@something} syntax already exists and, until now, it could only be usedinside an element/block, be it declaring a block-scoped constant with{@const}, rawdogging html with{@html}, or rendering with{@render}.
    Even debugging with{@debug} cannot be used in the opening tag like{@attachment}. This breaks sytax consistency and did not happen with the olduse: syntax as the oldsomething: syntax was always used on the opening tag of an element.
Black1358, panaman67, KadeR-jpg, stolinski, torrfura, and vlntsolo reacted with thumbs up emojimax-got, henrykrinkle01, kevinrenskers, hanielu, garth, umaranis, Maxiviper117, huntabyte, jjones315, skaldebane, and 8 more reacted with thumbs down emoji

@Ocean-OS
Copy link
Member

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>
max-got, bonclairvoyante, bhupeshpr25, llblab, gterras, cherlin, michal-weglarz-veyt, bcharbonnier, WarningImHack3r, oneezy, and 9 more reacted with thumbs up emoji

@itsmikesharescode
Copy link

I love it!

@Theo-Steiner
Copy link
Contributor

Theo-Steiner commentedJan 14, 2025
edited
Loading

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 likeisAttachmentKey is a helper to do just that!

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.

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!

JonathonRP reacted with thumbs up emoji

dummdidumm added a commit to sveltejs/prettier-plugin-svelte that referenced this pull requestMay 14, 2025
dummdidumm added a commit to sveltejs/prettier-plugin-svelte that referenced this pull requestMay 14, 2025
dummdidumm added a commit to sveltejs/language-tools that referenced this pull requestMay 14, 2025
@Rich-HarrisRich-Harris merged commitb93a617 intomainMay 14, 2025
13 checks passed
@Rich-HarrisRich-Harris deleted the attachments branchMay 14, 2025 17:33
@github-actionsgithub-actionsbot mentioned this pull requestMay 14, 2025
@mattpilott
Copy link
Contributor

Is it worth having a migrator for this? Will actions be deprecated?

@Ocean-OS
Copy link
Member

I don't think actions will be deprecated in the immediate future, but, likeclass:, it'll probably be deprecated at some point. Also, since there is a difference in how actions' and attachments' arguments work, it seems unlikely that a migrator tool could automate the process.

@mattpilott
Copy link
Contributor

Yes thats a good point, I wonder if there is a benefit to the built-in use:enhance action moving to a built-in attachment

@brunnerh
Copy link
Member

brunnerh commentedMay 14, 2025
edited
Loading

That would enable forwarding it through form wrapper components.

mattpilott and levibassey reacted with heart emoji

@Azarattum
Copy link
Contributor

Does this PR allow attaching transitions with attachments?

@Rich-Harris
Copy link
MemberAuthor

No. That would require an API for preventing effects from being destroyed; that hasn't been designed yet and will come later. After that, we can design a new transition API that builds on top of it and provides more flexibility than the current one.

JonathonRP, epavanello, gterras, and Ocean-OS reacted with thumbs up emojiAzarattum, vnphanquang, svenissimo, and jrmoynihan reacted with rocket emoji

@RyanYANG52
Copy link

Hi,@Rich-Harris . If I understand correctly, currentlyattachment is running inside an effect.

exportfunctionattach(node,get_fn){
effect(()=>{
constfn=get_fn();
// we use `&&` rather than `?.` so that things like
// `{@attach DEV && something_dev_only()}` work
returnfn&&fn(node);
});
}

I think this is very important to mention in theDOC , because it's diff fromevent handler andaction (action untrack)

current it only says

Since the tooltip(content) expression runs inside aneffect, the attachment will be destroyed and recreated whenever content changes.

I think it's unclear about what happens if state change inside attachment (return value oftooltip(content))

Another part isInline-attachments example, if attachment already inside an effect, why create another$effect

I am not sure attachment function run inside an effect is by design.

Best regards

@MotionlessTrain
Copy link
Contributor

I think it's unclear about what happens if state change inside attachment (return value of tooltip(content))

That is exactly what happens in the example (the content state is read inside of the attachment effect, and that is what makes it recreate itself

The nested effect makes sure that it doesn’t recreate itself, but update parts of it (another example I saw was something like tippy.setContent inside of an effect, to not recreate the tooltip)

dummdidumm reacted with thumbs up emoji

@RyanYANG52
Copy link

nested effect part I understand now, thanks. didn't find the doc about it before.

All I am saying is that maybe the doc counld be more clear about attachment is running inside an effect (not only factory part), currently only this

Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when the element is later removed from the DOM.

I can do something likethis, it still works.

sometime state is shared from outside, we need be aware of that, so we canuntrack or createnested effect to avoid unwanted rerun

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@LeonidazLeonidazLeonidaz left review comments

@brunnerhbrunnerhbrunnerh left review comments

@trueadmtrueadmtrueadm left review comments

@webJosewebJosewebJose left review comments

@Theo-SteinerTheo-SteinerTheo-Steiner left review comments

@dummdidummdummdidummdummdidumm approved these changes

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

45 participants
@Rich-Harris@huntabyte@JonathonRP@Leonidaz@dotmrjosh@Conduitry@Thiagolino8@kran6a@Ocean-OS@itsmikesharescode@Theo-Steiner@coryvirok@7nik@trueadm@bennymi@PuruVJ@bcharbonnier@henrikvilhelmberglund@ThePaSch@webJose@levibassey@Hugos68@jeremy-deutsch@jamesbirtles@gterras@bonclairvoyante@mrwokkel@paoloricciuti@dummdidumm@Not-Jayden@dangelomedinag@khromov@sdekna@razshare@brunnerh@qwuide@Pronian@ixxie@epavanello@kwangure@leninnava@mattpilott@Azarattum@RyanYANG52@MotionlessTrain

[8]ページ先頭

©2009-2025 Movatter.jp