Skip to content

Commit 1d6e20f

Browse files
authored
implement <svelte:fragment> (#4556)
add validation and test replace svelte:slot -> svelte:fragment slot as a sugar syntax fix eslint
1 parent c4479d9 commit 1d6e20f

File tree

109 files changed

+1193
-205
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+1193
-205
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Component from '../Component';
2+
import TemplateScope from './shared/TemplateScope';
3+
import Node from './shared/Node';
4+
import Let from './Let';
5+
import { INode } from './interfaces';
6+
7+
export default class DefaultSlotTemplate extends Node {
8+
type: 'SlotTemplate';
9+
scope: TemplateScope;
10+
children: INode[];
11+
lets: Let[] = [];
12+
slot_template_name = 'default';
13+
14+
constructor(
15+
component: Component,
16+
parent: INode,
17+
scope: TemplateScope,
18+
info: any,
19+
lets: Let[],
20+
children: INode[]
21+
) {
22+
super(component, parent, scope, info);
23+
this.type = 'SlotTemplate';
24+
this.children = children;
25+
this.scope = scope;
26+
this.lets = lets;
27+
}
28+
}

src/compiler/compile/nodes/Element.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export default class Element extends Node {
301301
component.slot_outlets.add(name);
302302
}
303303

304-
if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
304+
if (!(parent.type === 'SlotTemplate' || within_custom_element(parent))) {
305305
component.error(attribute, {
306306
code: 'invalid-slotted-content',
307307
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
@@ -906,6 +906,10 @@ export default class Element extends Node {
906906
);
907907
}
908908
}
909+
910+
get slot_template_name() {
911+
return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
912+
}
909913
}
910914

911915
function should_have_attribute(

src/compiler/compile/nodes/InlineComponent.ts

+52-7
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,6 @@ export default class InlineComponent extends Node {
4646
});
4747

4848
case 'Attribute':
49-
if (node.name === 'slot') {
50-
component.error(node, {
51-
code: 'invalid-prop',
52-
message: "'slot' is reserved for future use in named slots"
53-
});
54-
}
5549
// fallthrough
5650
case 'Spread':
5751
this.attributes.push(new Attribute(component, this, scope, node));
@@ -112,6 +106,57 @@ export default class InlineComponent extends Node {
112106
});
113107
});
114108

115-
this.children = map_children(component, this, this.scope, info.children);
109+
const children = [];
110+
for (let i=info.children.length - 1; i >= 0; i--) {
111+
const child = info.children[i];
112+
if (child.type === 'SlotTemplate') {
113+
children.push(child);
114+
info.children.splice(i, 1);
115+
} else if ((child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') && child.attributes.find(attribute => attribute.name === 'slot')) {
116+
const slot_template = {
117+
start: child.start,
118+
end: child.end,
119+
type: 'SlotTemplate',
120+
name: 'svelte:fragment',
121+
attributes: [],
122+
children: [child]
123+
};
124+
125+
// transfer attributes
126+
for (let i=child.attributes.length - 1; i >= 0; i--) {
127+
const attribute = child.attributes[i];
128+
if (attribute.type === 'Let') {
129+
slot_template.attributes.push(attribute);
130+
child.attributes.splice(i, 1);
131+
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
132+
slot_template.attributes.push(attribute);
133+
}
134+
}
135+
136+
children.push(slot_template);
137+
info.children.splice(i, 1);
138+
}
139+
}
140+
141+
if (info.children.some(node => not_whitespace_text(node))) {
142+
children.push({
143+
start: info.start,
144+
end: info.end,
145+
type: 'SlotTemplate',
146+
name: 'svelte:fragment',
147+
attributes: [],
148+
children: info.children
149+
});
150+
}
151+
152+
this.children = map_children(component, this, this.scope, children);
153+
}
154+
155+
get slot_template_name() {
156+
return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
116157
}
117158
}
159+
160+
function not_whitespace_text(node) {
161+
return !(node.type === 'Text' && /^\s+$/.test(node.data));
162+
}
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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_template_name: string = 'default';
16+
17+
constructor(
18+
component: Component,
19+
parent: INode,
20+
scope: TemplateScope,
21+
info: any
22+
) {
23+
super(component, parent, scope, info);
24+
25+
this.validate_slot_template_placement();
26+
27+
const has_let = info.attributes.some((node) => node.type === 'Let');
28+
if (has_let) {
29+
scope = scope.child();
30+
}
31+
32+
info.attributes.forEach((node) => {
33+
switch (node.type) {
34+
case 'Let': {
35+
const l = new Let(component, this, scope, node);
36+
this.lets.push(l);
37+
const dependencies = new Set([l.name.name]);
38+
39+
l.names.forEach((name) => {
40+
scope.add(name, dependencies, this);
41+
});
42+
break;
43+
}
44+
case 'Attribute': {
45+
if (node.name === 'slot') {
46+
this.slot_attribute = new Attribute(component, this, scope, node);
47+
if (!this.slot_attribute.is_static) {
48+
component.error(node, {
49+
code: 'invalid-slot-attribute',
50+
message: 'slot attribute cannot have a dynamic value'
51+
});
52+
}
53+
const value = this.slot_attribute.get_static_value();
54+
if (typeof value === 'boolean') {
55+
component.error(node, {
56+
code: 'invalid-slot-attribute',
57+
message: 'slot attribute value is missing'
58+
});
59+
}
60+
this.slot_template_name = value as string;
61+
break;
62+
}
63+
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
64+
}
65+
default:
66+
throw new Error(`Not implemented: ${node.type}`);
67+
}
68+
});
69+
70+
this.scope = scope;
71+
this.children = map_children(component, this, this.scope, info.children);
72+
}
73+
74+
validate_slot_template_placement() {
75+
if (this.parent.type !== 'InlineComponent') {
76+
this.component.error(this, {
77+
code: 'invalid-slotted-content',
78+
message: '<svelte:fragment> must be a child of a component'
79+
});
80+
}
81+
}
82+
}

src/compiler/compile/nodes/Text.ts

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

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

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

src/compiler/compile/nodes/interfaces.ts

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import Options from './Options';
2525
import PendingBlock from './PendingBlock';
2626
import RawMustacheTag from './RawMustacheTag';
2727
import Slot from './Slot';
28+
import SlotTemplate from './SlotTemplate';
29+
import DefaultSlotTemplate from './DefaultSlotTemplate';
2830
import Text from './Text';
2931
import ThenBlock from './ThenBlock';
3032
import Title from './Title';
@@ -58,6 +60,8 @@ export type INode = Action
5860
| PendingBlock
5961
| RawMustacheTag
6062
| Slot
63+
| SlotTemplate
64+
| DefaultSlotTemplate
6165
| Tag
6266
| Text
6367
| ThenBlock

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import ThenBlock from '../ThenBlock';
33
import CatchBlock from '../CatchBlock';
44
import InlineComponent from '../InlineComponent';
55
import Element from '../Element';
6+
import SlotTemplate from '../SlotTemplate';
67

7-
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element;
8+
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate;
89

910
export default class TemplateScope {
1011
names: Set<string>;
@@ -40,7 +41,7 @@ export default class TemplateScope {
4041

4142
is_let(name: string) {
4243
const owner = this.get_owner(name);
43-
return owner && (owner.type === 'Element' || owner.type === 'InlineComponent');
44+
return owner && (owner.type === 'Element' || owner.type === 'InlineComponent' || owner.type === 'SlotTemplate');
4445
}
4546

4647
is_await(name: string) {

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

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Options from '../Options';
1212
import RawMustacheTag from '../RawMustacheTag';
1313
import DebugTag from '../DebugTag';
1414
import Slot from '../Slot';
15+
import SlotTemplate from '../SlotTemplate';
1516
import Text from '../Text';
1617
import Title from '../Title';
1718
import Window from '../Window';
@@ -35,6 +36,7 @@ function get_constructor(type) {
3536
case 'RawMustacheTag': return RawMustacheTag;
3637
case 'DebugTag': return DebugTag;
3738
case 'Slot': return Slot;
39+
case 'SlotTemplate': return SlotTemplate;
3840
case 'Text': return Text;
3941
case 'Title': return Title;
4042
case 'Window': return Window;

src/compiler/compile/render_dom/wrappers/Element/create_slot_block.ts

-61
This file was deleted.

src/compiler/compile/render_dom/wrappers/Element/index.ts

-18
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { extract_names } from 'periscopic';
2525
import Action from '../../../nodes/Action';
2626
import MustacheTagWrapper from '../MustacheTag';
2727
import RawMustacheTagWrapper from '../RawMustacheTag';
28-
import create_slot_block from './create_slot_block';
2928
import is_dynamic from '../shared/is_dynamic';
3029

3130
interface BindingGroup {
@@ -142,7 +141,6 @@ export default class ElementWrapper extends Wrapper {
142141
event_handlers: EventHandler[];
143142
class_dependencies: string[];
144143

145-
slot_block: Block;
146144
select_binding_dependencies?: Set<string>;
147145

148146
var: any;
@@ -175,9 +173,6 @@ export default class ElementWrapper extends Wrapper {
175173
}
176174

177175
this.attributes = this.node.attributes.map(attribute => {
178-
if (attribute.name === 'slot') {
179-
block = create_slot_block(attribute, this, block);
180-
}
181176
if (attribute.name === 'style') {
182177
return new StyleAttributeWrapper(this, block, attribute);
183178
}
@@ -232,26 +227,13 @@ export default class ElementWrapper extends Wrapper {
232227
}
233228

234229
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
235-
236-
if (this.slot_block) {
237-
block.parent.add_dependencies(block.dependencies);
238-
239-
// appalling hack
240-
const index = block.parent.wrappers.indexOf(this);
241-
block.parent.wrappers.splice(index, 1);
242-
block.wrappers.push(this);
243-
}
244230
}
245231

246232
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
247233
const { renderer } = this;
248234

249235
if (this.node.name === 'noscript') return;
250236

251-
if (this.slot_block) {
252-
block = this.slot_block;
253-
}
254-
255237
const node = this.var;
256238
const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
257239
const children = x`@children(${this.node.name === 'template' ? x`${node}.content` : node})`;

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

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import InlineComponent from './InlineComponent/index';
1111
import MustacheTag from './MustacheTag';
1212
import RawMustacheTag from './RawMustacheTag';
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';
@@ -36,6 +37,7 @@ const wrappers = {
3637
Options: null,
3738
RawMustacheTag,
3839
Slot,
40+
SlotTemplate,
3941
Text,
4042
Title,
4143
Window

0 commit comments

Comments
 (0)