Skip to content

Commit 074e6d2

Browse files
committed
implement <svelte:slot>
1 parent 40987b7 commit 074e6d2

File tree

14 files changed

+216
-6
lines changed

14 files changed

+216
-6
lines changed
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import map_children from './shared/map_children';
2+
import Component from '../Component';
3+
import TemplateScope from './shared/TemplateScope';
4+
import Node from './shared/Node';
5+
import Let from './Let';
6+
import Attribute from './Attribute';
7+
import { INode } from './interfaces';
8+
9+
export default class SlotTemplate extends Node {
10+
type: 'SlotTemplate';
11+
scope: TemplateScope;
12+
children: INode[];
13+
lets: Let[] = [];
14+
slot_attribute: Attribute;
15+
slot_name: string = 'default';
16+
17+
constructor(component: Component, parent: INode, scope: TemplateScope, info: any) {
18+
super(component, parent, scope, info);
19+
20+
this.validate_slot_template_placement();
21+
22+
const has_let = info.attributes.some(node => node.type === 'Let');
23+
if (has_let) {
24+
scope = scope.child();
25+
}
26+
27+
info.attributes.forEach(node => {
28+
switch (node.type) {
29+
case 'Let': {
30+
const l = new Let(component, this, scope, node);
31+
this.lets.push(l);
32+
const dependencies = new Set([l.name.name]);
33+
34+
l.names.forEach(name => {
35+
scope.add(name, dependencies, this);
36+
});
37+
break;
38+
}
39+
case 'Attribute': {
40+
if (node.name === 'slot') {
41+
this.slot_attribute = new Attribute(component, this, scope, node);
42+
if (!this.slot_attribute.is_static) {
43+
component.error(node, {
44+
code: `invalid-slot-attribute`,
45+
message: `slot attribute cannot have a dynamic value`
46+
});
47+
}
48+
const value = this.slot_attribute.get_static_value();
49+
if (typeof value === 'boolean') {
50+
component.error(node, {
51+
code: `invalid-slot-attribute`,
52+
message: `slot attribute value is missing`
53+
});
54+
}
55+
this.slot_name = this.slot_attribute.get_static_value() as string;
56+
break;
57+
}
58+
throw new Error(`Invalid attribute "${node.name}" in <svelte:slot>`);
59+
}
60+
default:
61+
throw new Error(`Not implemented: ${node.type}`);
62+
}
63+
});
64+
65+
this.scope = scope;
66+
this.children = map_children(component, this, this.scope, info.children);
67+
}
68+
69+
validate_slot_template_placement() {
70+
if (this.parent.type !== 'InlineComponent') {
71+
this.component.error(this, {
72+
code: `invalid-slotted-content`,
73+
message: `Element with a slot='...' attribute must be a child of a component`
74+
});
75+
}
76+
}
77+
}

src/compiler/compile/nodes/Text.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default class Text extends Node {
2929
should_skip() {
3030
if (/\S/.test(this.data)) return false;
3131

32-
const parent_element = this.find_nearest(/(?:Element|InlineComponent|Head)/);
32+
const parent_element = this.find_nearest(/(?:Element|InlineComponent|SlotTemplate|Head)/);
3333
if (!parent_element) return false;
3434

3535
if (parent_element.type === 'Head') return true;

src/compiler/compile/nodes/interfaces.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Options from './Options';
2424
import PendingBlock from './PendingBlock';
2525
import RawMustacheTag from './RawMustacheTag';
2626
import Slot from './Slot';
27+
import SlotTemplate from './SlotTemplate';
2728
import Text from './Text';
2829
import ThenBlock from './ThenBlock';
2930
import Title from './Title';
@@ -56,6 +57,7 @@ export type INode = Action
5657
| PendingBlock
5758
| RawMustacheTag
5859
| Slot
60+
| SlotTemplate
5961
| Tag
6062
| Text
6163
| ThenBlock

src/compiler/compile/nodes/shared/map_children.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Options from '../Options';
1111
import RawMustacheTag from '../RawMustacheTag';
1212
import DebugTag from '../DebugTag';
1313
import Slot from '../Slot';
14+
import SlotTemplate from '../SlotTemplate';
1415
import Text from '../Text';
1516
import Title from '../Title';
1617
import Window from '../Window';
@@ -33,6 +34,7 @@ function get_constructor(type) {
3334
case 'RawMustacheTag': return RawMustacheTag;
3435
case 'DebugTag': return DebugTag;
3536
case 'Slot': return Slot;
37+
case 'SlotTemplate': return SlotTemplate;
3638
case 'Text': return Text;
3739
case 'Title': return Title;
3840
case 'Window': return Window;

src/compiler/compile/render_dom/wrappers/Fragment.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import InlineComponent from './InlineComponent/index';
1010
import MustacheTag from './MustacheTag';
1111
import RawMustacheTag from './RawMustacheTag';
1212
import Slot from './Slot';
13+
import SlotTemplate from './SlotTemplate';
1314
import Text from './Text';
1415
import Title from './Title';
1516
import Window from './Window';
@@ -34,6 +35,7 @@ const wrappers = {
3435
Options: null,
3536
RawMustacheTag,
3637
Slot,
38+
SlotTemplate,
3739
Text,
3840
Title,
3941
Window
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Wrapper from "./shared/Wrapper";
2+
import Renderer from "../Renderer";
3+
import Block from "../Block";
4+
import SlotTemplate from "../../nodes/SlotTemplate";
5+
import FragmentWrapper from "./Fragment";
6+
import create_debugging_comment from "./shared/create_debugging_comment";
7+
import { get_slot_definition } from './shared/get_slot_definition';
8+
import { x } from "code-red";
9+
import { sanitize } from "../../../utils/names";
10+
import { Identifier } from "estree";
11+
import InlineComponentWrapper from "./InlineComponent";
12+
13+
export default class SlotWrapper extends Wrapper {
14+
node: SlotTemplate;
15+
fragment: FragmentWrapper;
16+
block: Block;
17+
18+
constructor(
19+
renderer: Renderer,
20+
block: Block,
21+
parent: Wrapper,
22+
node: SlotTemplate,
23+
strip_whitespace: boolean,
24+
next_sibling: Wrapper
25+
) {
26+
super(renderer, block, parent, node);
27+
28+
const { scope, lets, slot_name } = this.node;
29+
30+
this.block = block.child({
31+
comment: create_debugging_comment(node, this.renderer.component),
32+
name: this.renderer.component.get_unique_name(
33+
`create_${sanitize(slot_name)}_slot`
34+
),
35+
type: "slot"
36+
});
37+
this.renderer.blocks.push(this.block);
38+
39+
if (this.parent.node.type === "InlineComponent") {
40+
const component = (this.parent as unknown) as InlineComponentWrapper;
41+
const seen = new Set(lets.map(l => l.name.name));
42+
component.node.lets.forEach(l => {
43+
if (!seen.has(l.name.name)) lets.push(l);
44+
});
45+
46+
component.slots.set(
47+
slot_name,
48+
get_slot_definition(this.block, scope, lets)
49+
);
50+
}
51+
52+
this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, strip_whitespace, next_sibling);
53+
54+
55+
this.block.parent.add_dependencies(this.block.dependencies);
56+
57+
// appalling hack, copied from ElementWrapper
58+
const index = this.block.parent.wrappers.indexOf(this);
59+
this.block.parent.wrappers.splice(index, 1);
60+
this.block.wrappers.push(this);
61+
}
62+
63+
render() {
64+
this.fragment.render(this.block, null, x`#nodes` as Identifier);
65+
}
66+
}

src/compiler/compile/render_dom/wrappers/shared/create_debugging_comment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function create_debugging_comment(
1515

1616
let d;
1717

18-
if (node.type === 'InlineComponent' || node.type === 'Element') {
18+
if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') {
1919
if (node.children.length) {
2020
d = node.children[0].start;
2121
while (source[d - 1] !== '>') d -= 1;

src/compiler/compile/render_ssr/Renderer.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import HtmlTag from './handlers/HtmlTag';
88
import IfBlock from './handlers/IfBlock';
99
import InlineComponent from './handlers/InlineComponent';
1010
import Slot from './handlers/Slot';
11+
import SlotTemplate from './handlers/SlotTemplate';
1112
import Tag from './handlers/Tag';
1213
import Text from './handlers/Text';
1314
import Title from './handlers/Title';
@@ -34,6 +35,7 @@ const handlers: Record<string, Handler> = {
3435
Options: noop,
3536
RawMustacheTag: HtmlTag,
3637
Slot,
38+
SlotTemplate,
3739
Text,
3840
Title,
3941
Window: noop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Renderer, { RenderOptions } from '../Renderer';
2+
import SlotTemplate from '../../nodes/SlotTemplate';
3+
import remove_whitespace_children from './utils/remove_whitespace_children';
4+
import { get_slot_scope } from './shared/get_slot_scope';
5+
import InlineComponent from '../../nodes/InlineComponent';
6+
7+
export default function(node: SlotTemplate, renderer: Renderer, options: RenderOptions & {
8+
slot_scopes: Map<any, any>;
9+
}) {
10+
const parent_inline_component = node.parent as InlineComponent;
11+
const children = remove_whitespace_children(node.children, node.next);
12+
13+
renderer.push();
14+
renderer.render(children, options);
15+
16+
const lets = node.lets;
17+
const seen = new Set(lets.map(l => l.name.name));
18+
parent_inline_component.lets.forEach(l => {
19+
if (!seen.has(l.name.name)) lets.push(l);
20+
});
21+
22+
options.slot_scopes.set(node.slot_name, {
23+
input: get_slot_scope(node.lets),
24+
output: renderer.pop()
25+
});
26+
}

src/compiler/parse/state/tag.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ const meta_tags = new Map([
1515
['svelte:head', 'Head'],
1616
['svelte:options', 'Options'],
1717
['svelte:window', 'Window'],
18-
['svelte:body', 'Body']
18+
['svelte:body', 'Body'],
1919
]);
2020

21-
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component');
21+
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:slot');
2222

2323
const specials = new Map([
2424
[
@@ -39,6 +39,7 @@ const specials = new Map([
3939

4040
const SELF = /^svelte:self(?=[\s/>])/;
4141
const COMPONENT = /^svelte:component(?=[\s/>])/;
42+
const SLOT = /^svelte:slot(?=[\s/>])/;
4243

4344
function parent_is_head(stack) {
4445
let i = stack.length;
@@ -107,8 +108,9 @@ export default function tag(parser: Parser) {
107108
const type = meta_tags.has(name)
108109
? meta_tags.get(name)
109110
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
110-
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
111-
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
111+
: name === 'svelte:slot' ? 'SlotTemplate'
112+
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
113+
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
112114

113115
const element: TemplateNode = {
114116
start,
@@ -259,6 +261,8 @@ function read_tag_name(parser: Parser) {
259261

260262
if (parser.read(COMPONENT)) return 'svelte:component';
261263

264+
if (parser.read(SLOT)) return 'svelte:slot';
265+
262266
const name = parser.read_until(/(\s|\/|>)/);
263267

264268
if (meta_tags.has(name)) return name;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
export let name;
3+
</script>
4+
5+
<div>Hello</div>
6+
<div>{name}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<div>a: <slot name="a"></slot></div>
2+
<div>b: <slot name="b"></slot></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
html: `
3+
<div>a: content <span>a</span></div>
4+
<div>b: <div>Hello</div><div>world</div></div>
5+
`
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
import Nested from "./Nested.svelte";
3+
import B from './B.svelte';
4+
const a = 'a';
5+
</script>
6+
<Nested>
7+
<svelte:slot slot="a">
8+
content <span>{ a }</span>
9+
</svelte:slot>
10+
<svelte:slot slot="b">
11+
<B name="world" />
12+
</svelte:slot>
13+
<svelte:slot slot="c">
14+
</svelte:slot>
15+
</Nested>

0 commit comments

Comments
 (0)