Skip to content

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
531e75d
parse attachments
Rich-Harris Dec 20, 2024
b29e1e3
basic attachments working
Rich-Harris Dec 20, 2024
2329284
working
Rich-Harris Dec 20, 2024
0c914eb
rename to attach
Rich-Harris Dec 20, 2024
1988ba4
fix
Rich-Harris Dec 21, 2024
e1b940c
restrict which symbols are recognised as attachment keys
Rich-Harris Dec 23, 2024
ed3bf01
merge main
Rich-Harris Jan 13, 2025
0690ba2
allow cleanup to be returned directly
Rich-Harris Jan 13, 2025
e0620a1
changeset
Rich-Harris Jan 13, 2025
2ae3aa0
fix
Rich-Harris Jan 13, 2025
7046427
lint
Rich-Harris Jan 14, 2025
85cc9bc
remove createAttachmentKey/isAttachmentKey
Rich-Harris Jan 14, 2025
bec5708
fix spreading of symbol properties onto component
Rich-Harris Jan 14, 2025
afab150
types
Rich-Harris Jan 14, 2025
7e5d4d9
fix
Rich-Harris Jan 14, 2025
c599e90
update name
Rich-Harris Jan 14, 2025
8699771
reserve ability to use sequence expressions in future
Rich-Harris Jan 14, 2025
1664fd8
Update packages/svelte/src/internal/client/dom/elements/attachments.js
Rich-Harris Jan 15, 2025
6402161
actually let's do this instead
Rich-Harris Jan 15, 2025
d09cdf6
expose createAttachmentKey
Rich-Harris May 13, 2025
8239d57
make room for `@attach` docs
Rich-Harris May 13, 2025
df3342f
add docs
Rich-Harris May 13, 2025
a466021
failing test
Rich-Harris May 13, 2025
d6d9f0c
fix
Rich-Harris May 13, 2025
5f7d9dd
lock down
Rich-Harris May 13, 2025
dd8c17a
merge/fix
Rich-Harris May 13, 2025
fe78c1c
add missing reference docs
Rich-Harris May 14, 2025
be46c94
prevent conflicts
Rich-Harris May 14, 2025
f8e6696
update docs
Rich-Harris May 14, 2025
b880f74
regenerate
Rich-Harris May 14, 2025
a7aa1a7
fix link
Rich-Harris May 14, 2025
bd0157a
add Attachment interface
Rich-Harris May 14, 2025
c69c5d7
beef up test
Rich-Harris May 14, 2025
999a05a
regenerate
Rich-Harris May 14, 2025
339ce84
tweak types
Rich-Harris May 14, 2025
8255bc6
fix
Rich-Harris May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-days-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: attachments
4 changes: 4 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
},
"./attachments": {
"types": "./types/index.d.ts",
"default": "./src/attachments/index.js"
},
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler/index.js",
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ await createBundle({
[pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/attachments`]: `${dir}/src/attachments/index.js`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/src/attachments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
create_attachment_key as createAttachmentKey,
is_attachment_key as isAttachmentKey
} from '../internal/client/dom/elements/attachments.js';
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/read/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/read/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
Expand Down
20 changes: 19 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,32 @@ function read_static_attribute(parser) {

/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
const start = parser.index;

if (parser.eat('{')) {
parser.allow_whitespace();

if (parser.eat('@attach')) {
parser.require_whitespace();

const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);

/** @type {AST.AttachTag} */
const use = {
type: 'AttachTag',
start,
end: parser.index,
expression
};

return use;
}

if (parser.eat('...')) {
const expression = read_expression(parser);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export function visit_component(node, context) {
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
attribute.type !== 'BindDirective' &&
attribute.type !== 'AttachTag'
) {
e.component_invalid_directive(attribute);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { AttachTag } from './visitors/AttachTag.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';

/** @type {Visitors} */
Expand Down Expand Up @@ -131,6 +132,7 @@ const visitors = {
TransitionDirective,
UpdateExpression,
UseDirective,
AttachTag,
VariableDeclaration
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function RegularElement(node, context) {
/** @type {AST.StyleDirective[]} */
const style_directives = [];

/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective>} */
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.AttachTag>} */
const other_directives = [];

/** @type {ExpressionStatement[]} */
Expand Down Expand Up @@ -152,6 +152,10 @@ export function RegularElement(node, context) {
has_use = true;
other_directives.push(attribute);
break;

case 'AttachTag':
other_directives.push(attribute);
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,16 @@ export function build_component(node, component_name, context, anchor = context.
);
}
}
} else if (attribute.type === 'AttachTag') {
// TODO do we need to create a derived here?
push_prop(
b.prop(
'get',
b.call('$.create_attachment_key'),
/** @type {Expression} */ (context.visit(attribute.expression)),
true
)
);
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ export namespace AST {
};
}

/** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode {
type: 'AttachTag';
expression: Expression;
}

/** An `animate:` directive */
export interface AnimateDirective extends BaseNode {
type: 'AnimateDirective';
Expand Down Expand Up @@ -273,7 +279,7 @@ export namespace AST {

interface BaseElement extends BaseNode {
name: string;
attributes: Array<Attribute | SpreadAttribute | Directive>;
attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
fragment: Fragment;
}

Expand Down Expand Up @@ -549,6 +555,7 @@ export namespace AST {
| AST.Attribute
| AST.SpreadAttribute
| Directive
| AST.AttachTag
| AST.Comment
| Block;

Expand Down
30 changes: 30 additions & 0 deletions packages/svelte/src/internal/client/dom/elements/attachments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { effect } from '../../reactivity/effects.js';

const key = `@attach-${/*@__PURE__*/ Math.random().toString(36).slice(2)}`;
const name = `Symbol(${key})`;

// TODO this feels a bit belt-and-braces to me, tbh — are we sure we need it?
/**
* Creates a `Symbol` that Svelte recognises as an attachment key
*/
export function create_attachment_key() {
return Symbol(key);
}

/**
* Returns `true` if the symbol was created with `createAttachmentKey`
* @param {string | symbol} key
*/
export function is_attachment_key(key) {
return typeof key === 'symbol' && key.toString() === name;
}

/**
* @param {Element} node
* @param {() => (node: Element) => void} get_fn
*/
export function attach(node, get_fn) {
effect(() => {
return get_fn()(node);
});
}
11 changes: 9 additions & 2 deletions packages/svelte/src/internal/client/dom/elements/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
set_active_effect,
set_active_reaction
} from '../../runtime.js';
import { attach, is_attachment_key } from './attachments.js';
import { clsx } from '../../../shared/attributes.js';

/**
Expand Down Expand Up @@ -245,8 +246,8 @@ export function set_custom_element_data(node, prop, value) {
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
* @param {Record<string, any> | undefined} prev
* @param {Record<string, any>} next New attributes - this function mutates this object
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [preserve_attribute_case]
* @param {boolean} [is_custom_element]
Expand Down Expand Up @@ -415,6 +416,12 @@ export function set_attributes(
}
}

for (let symbol of Object.getOwnPropertySymbols(next)) {
if (is_attachment_key(symbol)) {
attach(element, () => next[symbol]);
}
}

return current;
}

Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { append_styles } from './dom/css.js';
export { action } from './dom/elements/actions.js';
export { attach, create_attachment_key, is_attachment_key } from './dom/elements/attachments.js';
export {
remove_input_defaults,
set_attribute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div {@attach (node) => {}} {@attach (node) => {}}></div>
Loading
Loading