Skip to content

Commit

Permalink
implement <svelte:fragment> (#4556)
Browse files Browse the repository at this point in the history
add validation and test

replace svelte:slot -> svelte:fragment

slot as a sugar syntax

fix eslint
  • Loading branch information
tanhauhau authored Feb 27, 2021
1 parent c4479d9 commit 1d6e20f
Show file tree
Hide file tree
Showing 109 changed files with 1,193 additions and 205 deletions.
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

0 comments on commit 1d6e20f

Please sign in to comment.