Skip to content
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

Implement <svelte:fragment slot="name"> #4556

Merged
merged 1 commit into from
Feb 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions src/compiler/compile/nodes/DefaultSlotTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import { INode } from './interfaces';

export default class DefaultSlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
slot_template_name = 'default';

constructor(
component: Component,
parent: INode,
scope: TemplateScope,
info: any,
lets: Let[],
children: INode[]
) {
super(component, parent, scope, info);
this.type = 'SlotTemplate';
this.children = children;
this.scope = scope;
this.lets = lets;
}
}
6 changes: 5 additions & 1 deletion src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export default class Element extends Node {
component.slot_outlets.add(name);
}

if (!(parent.type === 'InlineComponent' || within_custom_element(parent))) {
if (!(parent.type === 'SlotTemplate' || within_custom_element(parent))) {
component.error(attribute, {
code: 'invalid-slotted-content',
message: 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
Expand Down Expand Up @@ -906,6 +906,10 @@ export default class Element extends Node {
);
}
}

get slot_template_name() {
return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
}
}

function should_have_attribute(
Expand Down
59 changes: 52 additions & 7 deletions src/compiler/compile/nodes/InlineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ export default class InlineComponent extends Node {
});

case 'Attribute':
if (node.name === 'slot') {
component.error(node, {
code: 'invalid-prop',
message: "'slot' is reserved for future use in named slots"
});
}
// fallthrough
case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node));
Expand Down Expand Up @@ -112,6 +106,57 @@ export default class InlineComponent extends Node {
});
});

this.children = map_children(component, this, this.scope, info.children);
const children = [];
for (let i=info.children.length - 1; i >= 0; i--) {
const child = info.children[i];
if (child.type === 'SlotTemplate') {
children.push(child);
info.children.splice(i, 1);
} else if ((child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') && child.attributes.find(attribute => attribute.name === 'slot')) {
const slot_template = {
start: child.start,
end: child.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: [child]
};

// transfer attributes
for (let i=child.attributes.length - 1; i >= 0; i--) {
const attribute = child.attributes[i];
if (attribute.type === 'Let') {
slot_template.attributes.push(attribute);
child.attributes.splice(i, 1);
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
slot_template.attributes.push(attribute);
}
}

children.push(slot_template);
info.children.splice(i, 1);
}
}

if (info.children.some(node => not_whitespace_text(node))) {
children.push({
start: info.start,
end: info.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: info.children
});
}

this.children = map_children(component, this, this.scope, children);
}

get slot_template_name() {
return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
}
}

function not_whitespace_text(node) {
return !(node.type === 'Text' && /^\s+$/.test(node.data));
}
82 changes: 82 additions & 0 deletions src/compiler/compile/nodes/SlotTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import map_children from './shared/map_children';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import Node from './shared/Node';
import Let from './Let';
import Attribute from './Attribute';
import { INode } from './interfaces';

export default class SlotTemplate extends Node {
type: 'SlotTemplate';
scope: TemplateScope;
children: INode[];
lets: Let[] = [];
slot_attribute: Attribute;
slot_template_name: string = 'default';

constructor(
component: Component,
parent: INode,
scope: TemplateScope,
info: any
) {
super(component, parent, scope, info);

this.validate_slot_template_placement();

const has_let = info.attributes.some((node) => node.type === 'Let');
if (has_let) {
scope = scope.child();
}

info.attributes.forEach((node) => {
switch (node.type) {
case 'Let': {
const l = new Let(component, this, scope, node);
this.lets.push(l);
const dependencies = new Set([l.name.name]);

l.names.forEach((name) => {
scope.add(name, dependencies, this);
});
break;
}
case 'Attribute': {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
});
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
component.error(node, {
code: 'invalid-slot-attribute',
message: 'slot attribute value is missing'
});
}
this.slot_template_name = value as string;
break;
}
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
}
default:
throw new Error(`Not implemented: ${node.type}`);
}
});

this.scope = scope;
this.children = map_children(component, this, this.scope, info.children);
}

validate_slot_template_placement() {
if (this.parent.type !== 'InlineComponent') {
this.component.error(this, {
code: 'invalid-slotted-content',
message: '<svelte:fragment> must be a child of a component'
});
}
}
}
2 changes: 1 addition & 1 deletion src/compiler/compile/nodes/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default class Text extends Node {
should_skip() {
if (/\S/.test(this.data)) return false;

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

if (parent_element.type === 'Head') return true;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/compile/nodes/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import Options from './Options';
import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import DefaultSlotTemplate from './DefaultSlotTemplate';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
Expand Down Expand Up @@ -58,6 +60,8 @@ export type INode = Action
| PendingBlock
| RawMustacheTag
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Tag
| Text
| ThenBlock
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/compile/nodes/shared/TemplateScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import ThenBlock from '../ThenBlock';
import CatchBlock from '../CatchBlock';
import InlineComponent from '../InlineComponent';
import Element from '../Element';
import SlotTemplate from '../SlotTemplate';

type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element;
type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Element | SlotTemplate;

export default class TemplateScope {
names: Set<string>;
Expand Down Expand Up @@ -40,7 +41,7 @@ export default class TemplateScope {

is_let(name: string) {
const owner = this.get_owner(name);
return owner && (owner.type === 'Element' || owner.type === 'InlineComponent');
return owner && (owner.type === 'Element' || owner.type === 'InlineComponent' || owner.type === 'SlotTemplate');
}

is_await(name: string) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/shared/map_children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Options from '../Options';
import RawMustacheTag from '../RawMustacheTag';
import DebugTag from '../DebugTag';
import Slot from '../Slot';
import SlotTemplate from '../SlotTemplate';
import Text from '../Text';
import Title from '../Title';
import Window from '../Window';
Expand All @@ -35,6 +36,7 @@ function get_constructor(type) {
case 'RawMustacheTag': return RawMustacheTag;
case 'DebugTag': return DebugTag;
case 'Slot': return Slot;
case 'SlotTemplate': return SlotTemplate;
case 'Text': return Text;
case 'Title': return Title;
case 'Window': return Window;
Expand Down

This file was deleted.

18 changes: 0 additions & 18 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { extract_names } from 'periscopic';
import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag';
import create_slot_block from './create_slot_block';
import is_dynamic from '../shared/is_dynamic';

interface BindingGroup {
Expand Down Expand Up @@ -142,7 +141,6 @@ export default class ElementWrapper extends Wrapper {
event_handlers: EventHandler[];
class_dependencies: string[];

slot_block: Block;
select_binding_dependencies?: Set<string>;

var: any;
Expand Down Expand Up @@ -175,9 +173,6 @@ export default class ElementWrapper extends Wrapper {
}

this.attributes = this.node.attributes.map(attribute => {
if (attribute.name === 'slot') {
block = create_slot_block(attribute, this, block);
}
if (attribute.name === 'style') {
return new StyleAttributeWrapper(this, block, attribute);
}
Expand Down Expand Up @@ -232,26 +227,13 @@ export default class ElementWrapper extends Wrapper {
}

this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);

if (this.slot_block) {
block.parent.add_dependencies(block.dependencies);

// appalling hack
const index = block.parent.wrappers.indexOf(this);
block.parent.wrappers.splice(index, 1);
block.wrappers.push(this);
}
}

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

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

if (this.slot_block) {
block = this.slot_block;
}

const node = this.var;
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
const children = x`@children(${this.node.name === 'template' ? x`${node}.content` : node})`;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import InlineComponent from './InlineComponent/index';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import Text from './Text';
import Title from './Title';
import Window from './Window';
Expand All @@ -36,6 +37,7 @@ const wrappers = {
Options: null,
RawMustacheTag,
Slot,
SlotTemplate,
Text,
Title,
Window
Expand Down
Loading