implement <svelte:fragment> (#4556)

add validation and test

replace svelte:slot -> svelte:fragment

slot as a sugar syntax

fix eslint
pull/6041/head
Tan Li Hau 4 years ago committed by GitHub
parent c4479d976b
commit 1d6e20f2a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;
}
}

@ -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'
@ -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(

@ -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));
@ -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));
}

@ -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'
});
}
}
}

@ -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;

@ -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';
@ -58,6 +60,8 @@ export type INode = Action
| PendingBlock
| RawMustacheTag
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Tag
| Text
| ThenBlock

@ -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>;
@ -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) {

@ -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';
@ -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;

@ -1,61 +0,0 @@
import ElementWrapper from './index';
import SlotWrapper from '../Slot';
import Block from '../../Block';
import { sanitize } from '../../../../utils/names';
import InlineComponentWrapper from '../InlineComponent';
import create_debugging_comment from '../shared/create_debugging_comment';
import { get_slot_definition } from '../shared/get_slot_definition';
export default function create_slot_block(attribute, element: ElementWrapper | SlotWrapper, block: Block) {
const owner = find_slot_owner(element.parent);
if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.get_static_value() as string;
if (!((owner as unknown) as InlineComponentWrapper).slots.has(name)) {
const child_block = block.child({
comment: create_debugging_comment(element.node, element.renderer.component),
name: element.renderer.component.get_unique_name(
`create_${sanitize(name)}_slot`
),
type: 'slot'
});
const { scope, lets } = element.node;
const seen = new Set(lets.map(l => l.name.name));
((owner as unknown) as InlineComponentWrapper).node.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
((owner as unknown) as InlineComponentWrapper).slots.set(
name,
get_slot_definition(child_block, scope, lets)
);
element.renderer.blocks.push(child_block);
}
element.slot_block = ((owner as unknown) as InlineComponentWrapper).slots.get(
name
).block;
return element.slot_block;
}
return block;
}
function find_slot_owner(owner) {
while (owner) {
if (owner.node.type === 'InlineComponent') {
break;
}
if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
break;
}
owner = owner.parent;
}
return owner;
}

@ -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 {
@ -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;
@ -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);
}
@ -232,15 +227,6 @@ 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) {
@ -248,10 +234,6 @@ export default class ElementWrapper extends Wrapper {
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})`;

@ -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';
@ -36,6 +37,7 @@ const wrappers = {
Options: null,
RawMustacheTag,
Slot,
SlotTemplate,
Text,
Title,
Window

@ -4,12 +4,11 @@ import Renderer from '../../Renderer';
import Block from '../../Block';
import InlineComponent from '../../../nodes/InlineComponent';
import FragmentWrapper from '../Fragment';
import SlotTemplateWrapper from '../SlotTemplate';
import { sanitize } from '../../../../utils/names';
import add_to_set from '../../../utils/add_to_set';
import { b, x, p } from 'code-red';
import Attribute from '../../../nodes/Attribute';
import create_debugging_comment from '../shared/create_debugging_comment';
import { get_slot_definition } from '../shared/get_slot_definition';
import TemplateScope from '../../../nodes/shared/TemplateScope';
import is_dynamic from '../shared/is_dynamic';
import bind_this from '../shared/bind_this';
@ -18,12 +17,16 @@ import EventHandler from '../Element/EventHandler';
import { extract_names } from 'periscopic';
import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import { string_to_member_expression } from '../../../utils/string_to_member_expression';
import SlotTemplate from '../../../nodes/SlotTemplate';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
export default class InlineComponentWrapper extends Wrapper {
var: Identifier;
slots: Map<string, { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }> = new Map();
slots: Map<string, SlotDefinition> = new Map();
node: InlineComponent;
fragment: FragmentWrapper;
children: Array<Wrapper | FragmentWrapper> = [];
constructor(
renderer: Renderer,
@ -76,32 +79,22 @@ export default class InlineComponentWrapper extends Wrapper {
});
});
const default_slot = block.child({
comment: create_debugging_comment(node, renderer.component),
name: renderer.component.get_unique_name('create_default_slot'),
type: 'slot'
});
this.renderer.blocks.push(default_slot);
this.slots.set('default', get_slot_definition(default_slot, this.node.scope, this.node.lets));
this.fragment = new FragmentWrapper(renderer, default_slot, node.children, this, strip_whitespace, next_sibling);
const dependencies: Set<string> = new Set();
// TODO is this filtering necessary? (I *think* so)
default_slot.dependencies.forEach(name => {
if (!this.node.scope.is_let(name)) {
dependencies.add(name);
}
});
block.add_dependencies(dependencies);
this.children = this.node.children.map(child => new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling));
}
block.add_outro();
}
set_slot(name: string, slot_definition: SlotDefinition) {
if (this.slots.has(name)) {
if (name === 'default') {
throw new Error('Found elements without slot attribute when using slot="default"');
}
throw new Error(`Duplicate slot name "${name}" in <${this.node.name}>`);
}
this.slots.set(name, slot_definition);
}
warn_if_reactive() {
const { name } = this.node;
const variable = this.renderer.component.var_lookup.get(name);
@ -135,14 +128,10 @@ export default class InlineComponentWrapper extends Wrapper {
const statements: Array<Node | Node[]> = [];
const updates: Array<Node | Node[]> = [];
if (this.fragment) {
this.children.forEach((child) => {
this.renderer.add_to_context('$$scope', true);
const default_slot = this.slots.get('default');
this.fragment.nodes.forEach((child) => {
child.render(default_slot.block, null, x`#nodes` as unknown as Identifier);
});
}
child.render(block, null, x`#nodes` as Identifier);
});
let props;
const name_changes = block.get_unique_name(`${name.name}_changes`);
@ -194,7 +183,7 @@ export default class InlineComponentWrapper extends Wrapper {
component_opts.properties.push(p`$$inline: true`);
}
const fragment_dependencies = new Set(this.fragment ? ['$$scope'] : []);
const fragment_dependencies = new Set(this.slots.size ? ['$$scope'] : []);
this.slots.forEach(slot => {
slot.block.dependencies.forEach(name => {
const is_let = slot.scope.is_let(name);

@ -11,7 +11,6 @@ import { is_reserved_keyword } from '../../utils/reserved_keywords';
import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression } from 'estree';
import create_debugging_comment from './shared/create_debugging_comment';
import create_slot_block from './Element/create_slot_block';
export default class SlotWrapper extends Wrapper {
node: Slot;
@ -43,10 +42,6 @@ export default class SlotWrapper extends Wrapper {
renderer.blocks.push(this.fallback);
}
if (this.node.values.has('slot')) {
block = create_slot_block(this.node.values.get('slot'), this, block);
}
this.fragment = new FragmentWrapper(
renderer,
this.fallback,

@ -0,0 +1,80 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import FragmentWrapper from './Fragment';
import create_debugging_comment from './shared/create_debugging_comment';
import { get_slot_definition } from './shared/get_slot_definition';
import { x } from 'code-red';
import { sanitize } from '../../../utils/names';
import { Identifier } from 'estree';
import InlineComponentWrapper from './InlineComponent';
import { extract_names } from 'periscopic';
import { INode } from '../../nodes/interfaces';
import Let from '../../nodes/Let';
import TemplateScope from '../../nodes/shared/TemplateScope';
type NodeWithLets = INode & {
scope: TemplateScope;
lets: Let[];
slot_template_name: string;
};
export default class SlotTemplateWrapper extends Wrapper {
node: NodeWithLets;
fragment: FragmentWrapper;
block: Block;
parent: InlineComponentWrapper;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: NodeWithLets,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
const { scope, lets, slot_template_name } = this.node;
lets.forEach(l => {
extract_names(l.value || l.name).forEach(name => {
renderer.add_to_context(name, true);
});
});
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: this.renderer.component.get_unique_name(
`create_${sanitize(slot_template_name)}_slot`
),
type: 'slot'
});
this.renderer.blocks.push(this.block);
const seen = new Set(lets.map(l => l.name.name));
this.parent.node.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
this.parent.set_slot(
slot_template_name,
get_slot_definition(this.block, scope, lets)
);
this.fragment = new FragmentWrapper(
renderer,
this.block,
node.type === 'SlotTemplate' ? node.children : [node],
this,
strip_whitespace,
next_sibling
);
this.block.parent.add_dependencies(this.block.dependencies);
}
render() {
this.fragment.render(this.block, null, x`#nodes` as Identifier);
}
}

@ -15,7 +15,7 @@ export default function create_debugging_comment(
let d;
if (node.type === 'InlineComponent' || node.type === 'Element') {
if (node.type === 'InlineComponent' || node.type === 'Element' || node.type === 'SlotTemplate') {
if (node.children.length) {
d = node.children[0].start;
while (source[d - 1] !== '>') d -= 1;

@ -9,6 +9,7 @@ import IfBlock from './handlers/IfBlock';
import InlineComponent from './handlers/InlineComponent';
import KeyBlock from './handlers/KeyBlock';
import Slot from './handlers/Slot';
import SlotTemplate from './handlers/SlotTemplate';
import Tag from './handlers/Tag';
import Text from './handlers/Text';
import Title from './handlers/Title';
@ -36,6 +37,7 @@ const handlers: Record<string, Handler> = {
Options: noop,
RawMustacheTag: HtmlTag,
Slot,
SlotTemplate,
Text,
Title,
Window: noop

@ -1,6 +1,5 @@
import { is_void } from '../../../utils/names';
import { get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { get_slot_scope } from './shared/get_slot_scope';
import { boolean_attributes } from './shared/boolean_attributes';
import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element';
@ -8,9 +7,7 @@ import { x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
export default function(node: Element, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
export default function(node: Element, renderer: Renderer, options: RenderOptions) {
const children = remove_whitespace_children(node.children, node.next);
@ -23,13 +20,6 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
node.attributes.some((attribute) => attribute.name === 'contenteditable')
);
const slot = node.get_static_attribute_value('slot');
const nearest_inline_component = node.find_nearest(/InlineComponent/);
if (slot && nearest_inline_component) {
renderer.push();
}
renderer.add_string(`<${node.name}`);
const class_expression_list = node.classes.map(class_directive => {
@ -148,24 +138,6 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
} else if (slot && nearest_inline_component) {
renderer.render(children, options);
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
const lets = node.lets;
const seen = new Set(lets.map(l => l.name.name));
nearest_inline_component.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
options.slot_scopes.set(slot, {
input: get_slot_scope(node.lets),
output: renderer.pop()
});
} else {
renderer.render(children, options);

@ -1,8 +1,6 @@
import { string_literal } from '../../utils/stringify';
import Renderer, { RenderOptions } from '../Renderer';
import { get_slot_scope } from './shared/get_slot_scope';
import InlineComponent from '../../nodes/InlineComponent';
import remove_whitespace_children from './utils/remove_whitespace_children';
import { p, x } from 'code-red';
function get_prop_value(attribute) {
@ -68,28 +66,19 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
const slot_fns = [];
const children = remove_whitespace_children(node.children, node.next);
const children = node.children;
if (children.length) {
const slot_scopes = new Map();
renderer.push();
renderer.render(children, Object.assign({}, options, {
slot_scopes
}));
slot_scopes.set('default', {
input: get_slot_scope(node.lets),
output: renderer.pop()
});
slot_scopes.forEach(({ input, output }, name) => {
if (!is_empty_template_literal(output)) {
slot_fns.push(
p`${name}: (${input}) => ${output}`
);
}
slot_fns.push(
p`${name}: (${input}) => ${output}`
);
});
}
@ -99,11 +88,3 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`);
}
function is_empty_template_literal(template_literal) {
return (
template_literal.expressions.length === 0 &&
template_literal.quasis.length === 1 &&
template_literal.quasis[0].value.raw === ''
);
}

@ -0,0 +1,45 @@
import Renderer, { RenderOptions } from '../Renderer';
import SlotTemplate from '../../nodes/SlotTemplate';
import remove_whitespace_children from './utils/remove_whitespace_children';
import { get_slot_scope } from './shared/get_slot_scope';
import InlineComponent from '../../nodes/InlineComponent';
import Element from '../../nodes/Element';
export default function(node: SlotTemplate | Element | InlineComponent, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
const parent_inline_component = node.parent as InlineComponent;
const children = remove_whitespace_children(node instanceof SlotTemplate ? node.children : [node], node.next);
renderer.push();
renderer.render(children, options);
const lets = node.lets;
const seen = new Set(lets.map(l => l.name.name));
parent_inline_component.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
const slot_fragment_content = renderer.pop();
if (!is_empty_template_literal(slot_fragment_content)) {
if (options.slot_scopes.has(node.slot_template_name)) {
if (node.slot_template_name === 'default') {
throw new Error('Found elements without slot attribute when using slot="default"');
}
throw new Error(`Duplicate slot name "${node.slot_template_name}" in <${parent_inline_component.name}>`);
}
options.slot_scopes.set(node.slot_template_name, {
input: get_slot_scope(node.lets),
output: slot_fragment_content
});
}
}
function is_empty_template_literal(template_literal) {
return (
template_literal.expressions.length === 0 &&
template_literal.quasis.length === 1 &&
template_literal.quasis[0].value.raw === ''
);
}

@ -18,7 +18,7 @@ const meta_tags = new Map([
['svelte:body', 'Body']
]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component');
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment');
const specials = new Map([
[
@ -39,6 +39,7 @@ const specials = new Map([
const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
function parent_is_head(stack) {
let i = stack.length;
@ -107,8 +108,9 @@ export default function tag(parser: Parser) {
const type = meta_tags.has(name)
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
: name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: TemplateNode = {
start,
@ -265,6 +267,8 @@ function read_tag_name(parser: Parser) {
if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(/(\s|\/|>)/);
if (meta_tags.has(name)) return name;

@ -1,6 +1,6 @@
{
"code": "invalid-tag-name",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self or svelte:component",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component or svelte:fragment",
"pos": 10,
"start": {
"character": 10,

@ -0,0 +1,15 @@
export default {
html: `
<button>Disable</button>
<button slot="footer">Button</button>
<button slot="footer">Button</button>
`,
async test({ assert, component, target, window }) {
const [btn, btn1, btn2] = target.querySelectorAll('button');
await btn.dispatchEvent(new window.MouseEvent('click'));
assert.equal(btn1.disabled, true);
assert.equal(btn2.disabled, true);
}
};

@ -0,0 +1,15 @@
<script>
import Component from './Component.svelte';
let disabled = false;
</script>
<button on:click={() => disabled = !disabled}>Disable</button>
<Component>
<button slot="footer" disabled={disabled}>Button</button>
</Component>
<Component>
<button disabled={disabled} slot="footer">Button</button>
</Component>

@ -0,0 +1,5 @@
<script>
export let name;
</script>
<span>Hello {name}</span>

@ -0,0 +1,7 @@
export default {
preserveIdentifiers: true,
html: `
<span>Hello world</span>
`
};

@ -0,0 +1,10 @@
<script>
import Nested from './Nested.svelte';
import Hello from './Hello.svelte';
let name = 'world';
</script>
<Nested>
<Hello slot="name" {name} />
</Nested>

@ -0,0 +1,6 @@
export default {
html: `
<span>Hello</span>
<span>world</span>
`
};

@ -0,0 +1,13 @@
<script>
import Nested from './Nested.svelte';
import Hello from './Hello.svelte';
import World from './World.svelte';
</script>
<Nested>
<Hello slot="name" />
</Nested>
<Nested>
<World slot="name" />
</Nested>

@ -0,0 +1,5 @@
<div>
<slot/>
<slot name='bar'/>
<slot name='foo'/>
</div>

@ -0,0 +1,9 @@
export default {
html: `
<div>
Hello
<p>bar</p>
<p>foo</p>
</div>
`
};

@ -0,0 +1,12 @@
<script>
import Nested from './Nested.svelte';
import Foo from './Foo.svelte';
import Bar from './Bar.svelte';
</script>
<Nested>
Hello
<Foo slot='foo' />
<Bar slot='bar' />
</Nested>

@ -0,0 +1,3 @@
export default {
error: 'Duplicate slot name "foo" in <Nested>'
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="foo">{value}</svelte:fragment>
<svelte:fragment slot="foo">{value}</svelte:fragment>
</Nested>

@ -0,0 +1,3 @@
export default {
error: 'Duplicate slot name "foo" in <Nested>'
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="foo">{value}</svelte:fragment>
<p slot="foo">{value}</p>
</Nested>

@ -0,0 +1,3 @@
export default {
error: 'Found elements without slot attribute when using slot="default"'
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="default">value</svelte:fragment>
<p>value</p>
</Nested>

@ -0,0 +1,3 @@
export default {
error: 'Duplicate slot name "foo" in <Nested>'
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<p slot="foo">{value}</p>
<p slot="foo">{value}</p>
</Nested>

@ -3,7 +3,6 @@ export default {
dev: true
},
warnings: [
'<Nested> received an unexpected slot "default".',
'<Nested> received an unexpected slot "slot1".'
]
};

@ -0,0 +1,6 @@
export default {
html: `
<span>Hello</span>
<span>world</span>
`
};

@ -0,0 +1,15 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="name">
<span>Hello</span>
</svelte:fragment>
</Nested>
<Nested>
<svelte:fragment slot="name">
<span>world</span>
</svelte:fragment>
</Nested>

@ -0,0 +1,9 @@
<script>
export let things;
</script>
<div>
{#each things as thing}
<slot name="main" {thing}/>
{/each}
</div>

@ -0,0 +1,24 @@
export default {
props: {
things: [1, 2, 3]
},
html: `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>`,
test({ assert, component, target }) {
component.things = [1, 2, 3, 4];
assert.htmlEqual(target.innerHTML, `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
import Nested from './Nested.svelte';
export let things;
</script>
<Nested {things} let:thing={x}>
<svelte:fragment slot="main">
<span>{x}</span>
</svelte:fragment>
</Nested>

@ -0,0 +1,6 @@
<script>
let count = 0;
</script>
<button on:click="{() => count += 1}">+1</button>
<slot name="main" count={count}/>

@ -0,0 +1,18 @@
export default {
html: `
<button>+1</button>
<span>0</span>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
await button.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>+1</button>
<span>1</span>
`);
}
};

@ -0,0 +1,9 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested let:count>
<svelte:fragment slot="main">
<span>{count}</span>
</svelte:fragment>
</Nested>

@ -0,0 +1,6 @@
<script>
let count = 0;
</script>
<button on:click="{() => count += 1}">+1</button>
<slot name='main' c={count}/>

@ -0,0 +1,18 @@
export default {
html: `
<button>+1</button>
<span>0 (undefined)</span>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
await button.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>+1</button>
<span>1 (undefined)</span>
`);
}
};

@ -0,0 +1,9 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="main" let:c let:count>
<span>{c} ({count})</span>
</svelte:fragment>
</Nested>

@ -0,0 +1,7 @@
<script>
let foo = 'a';
</script>
<div on:click="{() => foo = 'b'}">
<slot name="main" {foo}></slot>
</div>

@ -0,0 +1,20 @@
export default {
html: `
<div>
<p>a</p>
</div>
`,
async test({ assert, target, window }) {
const div = target.querySelector('div');
const click = new window.MouseEvent('click');
await div.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<div>
<p>b</p>
</div>
`);
}
};

@ -0,0 +1,9 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="main" let:foo={bar}>
<p>{bar}</p>
</svelte:fragment>
</Nested>

@ -0,0 +1,5 @@
<script>
export let props;
</script>
<slot name="main" value={props} data={Array.isArray(props) ? props[0] : props.a} />

@ -0,0 +1,68 @@
export default {
html: `
<div>
hello world 0 hello
<button>Increment</button>
</div>
<div>
hello world 0 hello
<button>Increment</button>
</div>
<div>
hello world 0 hello
<button>Increment</button>
</div>
`,
async test({ assert, component, target, window }) {
const [button1, button2, button3] = target.querySelectorAll('button');
const event = new window.MouseEvent('click');
await button1.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<div>
hello world 1 hello
<button>Increment</button>
</div>
<div>
hello world 0 hello
<button>Increment</button>
</div>
<div>
hello world 0 hello
<button>Increment</button>
</div>
`);
await button2.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<div>
hello world 1 hello
<button>Increment</button>
</div>
<div>
hello world 1 hello
<button>Increment</button>
</div>
<div>
hello world 0 hello
<button>Increment</button>
</div>
`);
await button3.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<div>
hello world 1 hello
<button>Increment</button>
</div>
<div>
hello world 1 hello
<button>Increment</button>
</div>
<div>
hello world 1 hello
<button>Increment</button>
</div>
`);
}
};

@ -0,0 +1,34 @@
<script>
import Nested from "./Nested.svelte";
let c = 0, d = 0, e = 0;
</script>
<div>
<Nested props={['hello', 'world']}>
<svelte:fragment slot="main" let:value={pair} let:data={foo}>
{pair[0]} {pair[1]} {c} {foo}
</svelte:fragment>
</Nested>
<button on:click={() => { c += 1; }}>Increment</button>
</div>
<div>
<Nested props={['hello', 'world']}>
<svelte:fragment slot="main" let:value={[a, b]} let:data={foo}>
{a} {b} {d} {foo}
</svelte:fragment>
</Nested>
<button on:click={() => { d += 1; }}>Increment</button>
</div>
<div>
<Nested props={{ a: 'hello', b: 'world' }}>
<svelte:fragment slot="main" let:value={{ a, b }} let:data={foo}>
{a} {b} {e} {foo}
</svelte:fragment>
</Nested>
<button on:click={() => { e += 1; }}>Increment</button>
</div>

@ -0,0 +1,9 @@
<script>
export let things;
</script>
<div>
{#each things as thing}
<slot name="item" {thing}/>
{/each}
</div>

@ -0,0 +1,34 @@
export default {
props: {
things: [
{ num: 1 },
{ num: 2 },
{ num: 3 }
]
},
html: `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>`,
test({ assert, component, target }) {
component.things = [
{ num: 1 },
{ num: 2 },
{ num: 3 },
{ num: 4 }
];
assert.htmlEqual(target.innerHTML, `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
import Nested from './Nested.svelte';
export let things;
</script>
<Nested {things}>
<svelte:fragment slot="item" let:thing="{{ num }}">
<span>{num}</span>
</svelte:fragment>
</Nested>

@ -0,0 +1,11 @@
<script>
import B from './B.svelte';
export let x;
</script>
<B {x} let:reflected>
<svelte:fragment slot="main">
<span>{reflected}</span>
<slot name="main" {reflected} />
</svelte:fragment>
</B>

@ -0,0 +1,5 @@
<script>
export let x;
</script>
<slot name="main" reflected={x}/>

@ -0,0 +1,19 @@
export default {
html: `
<span>1</span>
<span>1</span>
<span>1</span>
<span>1</span>
`,
async test({ assert, target, component }) {
component.x = 2;
assert.htmlEqual(target.innerHTML, `
<span>2</span>
<span>2</span>
<span>2</span>
<span>2</span>
`);
}
};

@ -0,0 +1,16 @@
<script>
import A from './A.svelte';
export let x = 1;
</script>
<A {x}>
<svelte:fragment slot="main" let:reflected>
<span>{reflected}</span>
</svelte:fragment>
</A>
<A {x} let:reflected>
<svelte:fragment slot="main">
<span>{reflected}</span>
</svelte:fragment>
</A>

@ -0,0 +1,5 @@
<script>
export let x;
</script>
<slot name="foo" reflected={x}/>

@ -0,0 +1,22 @@
export default {
html: `
<span class="1">1</span>
0
`,
async test({ assert, target, component, window }) {
component.x = 2;
assert.htmlEqual(target.innerHTML, `
<span class="2">2</span>
0
`);
const span = target.querySelector('span');
await span.dispatchEvent(new window.MouseEvent('click'));
assert.htmlEqual(target.innerHTML, `
<span class="2">2</span>
2
`);
}
};

@ -0,0 +1,17 @@
<script>
import A from './A.svelte';
export let x = 1;
let y = 0;
</script>
<A {x}>
<svelte:fragment slot="foo" let:reflected>
<span
on:click={() => y = reflected}
class={reflected}
>
{reflected}
</span>
</svelte:fragment>
</A>
{ y }

@ -0,0 +1,9 @@
<script>
export let items;
</script>
<div>
{#each items as item, index}
<slot name="main" {index}/>
{/each}
</div>

@ -0,0 +1,26 @@
export default {
html: `
<div>
<label>1: <input></label>
<label>2: <input></label>
<label>3: <input></label>
</div>
`,
ssrHtml: `
<div>
<label>1: <input value="a"></label>
<label>2: <input value="b"></label>
<label>3: <input value="c"></label>
</div>
`,
async test({ assert, component, target, window }) {
const inputs = target.querySelectorAll('input');
inputs[2].value = 'd';
await inputs[2].dispatchEvent(new window.Event('input'));
assert.deepEqual(component.letters, ['a', 'b', 'd']);
}
};

@ -0,0 +1,13 @@
<script>
import Nested from './Nested.svelte';
export let letters = ['a', 'b', 'c'];
</script>
<Nested items={letters}>
<svelte:fragment slot="main" let:index>
<label>
{index + 1}: <input bind:value={letters[index]}>
</label>
</svelte:fragment>
</Nested>

@ -0,0 +1,5 @@
<script>
export let prop
</script>
<slot name="main" value={prop} />

@ -0,0 +1,12 @@
export default {
props: {
prop: 'a'
},
html: 'a',
test({ assert, component, target }) {
component.prop = 'b';
assert.htmlEqual( target.innerHTML, 'b' );
}
};

@ -0,0 +1,16 @@
<script>
import Outer from './Outer.svelte'
import Inner from './Inner.svelte'
export let prop
</script>
<Outer {prop}>
<svelte:fragment slot="main" let:value>
<Inner>
<svelte:fragment slot="main">
{value}
</svelte:fragment>
</Inner>
</svelte:fragment>
</Outer>

@ -0,0 +1,9 @@
<script>
export let things;
</script>
<div>
{#each things as thing}
<slot name="foo" {thing}/>
{/each}
</div>

@ -0,0 +1,24 @@
export default {
props: {
things: [1, 2, 3]
},
html: `
<div>
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
<div slot="foo"><span>3</span></div>
</div>`,
test({ assert, component, target }) {
component.things = [1, 2, 3, 4];
assert.htmlEqual(target.innerHTML, `
<div>
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
<div slot="foo"><span>3</span></div>
<div slot="foo"><span>4</span></div>
</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
import Nested from './Nested.svelte';
export let things;
</script>
<Nested {things}>
<div slot="foo" let:thing>
<span>{thing}</span>
</div>
</Nested>

@ -0,0 +1,3 @@
export default {
html: '<p>Hi</p>'
};

@ -0,0 +1,7 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested let:value>
<p>{value}</p>
</Nested>

@ -0,0 +1,9 @@
<script>
export let things;
</script>
<div>
{#each things as thing}
<slot {thing}/>
{/each}
</div>

@ -0,0 +1,24 @@
export default {
props: {
things: [1, 2, 3]
},
html: `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>`,
test({ assert, component, target }) {
component.things = [1, 2, 3, 4];
assert.htmlEqual(target.innerHTML, `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
`);
}
};

@ -0,0 +1,9 @@
<script>
import Nested from './Nested.svelte';
export let things;
</script>
<Nested {things} let:thing>
<span>{thing}</span>
</Nested>

@ -0,0 +1,15 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested>
<svelte:fragment slot="name">
<slot />
</svelte:fragment>
</Nested>
<Nested>
<svelte:fragment slot="name">
<slot name="b" />
</svelte:fragment>
</Nested>

@ -0,0 +1,6 @@
export default {
html: `
Default
<p>B slot</p>
`
};

@ -0,0 +1,12 @@
<script>
import Child from './Child.svelte';
</script>
<Child>
<svelte:fragment>
Default
</svelte:fragment>
<svelte:fragment slot="b">
<p>B slot</p>
</svelte:fragment>
</Child>

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<div>Hello</div>
<div>{name}</div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save