diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index b0ffead6e0..880d3a7879 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -289,5 +289,16 @@ export default { invalid_style_directive_modifier: (valid: string) => ({ code: 'invalid-style-directive-modifier', message: `Valid modifiers for style directives are: ${valid}` - }) + }), + invalid_mix_element_and_conditional_slot: { + code: 'invalid-mix-element-and-conditional-slot', + message: 'Do not mix and other elements under the same {#if}{:else} group. Default slot content should be wrapped with ' + }, + duplicate_slot_name_in_component: (slot_name: string, component_name: string) => ({ + code: 'duplicate-slot-name-in-component', + message: + slot_name === "default" + ? 'Found elements without slot attribute when using slot="default"' + : `Duplicate slot name "${slot_name}" in <${component_name}>`, + }), }; diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index e9ac86a55c..8651ba170a 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -11,6 +11,8 @@ import { INode } from './interfaces'; import { TemplateNode } from '../../interfaces'; import compiler_errors from '../compiler_errors'; import { regex_only_whitespaces } from '../../utils/patterns'; +import SlotTemplateIfBlock, { validate_get_slot_names } from './SlotTemplateIfBlock'; +import SlotTemplate from './SlotTemplate'; export default class InlineComponent extends Node { type: 'InlineComponent'; @@ -52,7 +54,7 @@ export default class InlineComponent extends Node { this.css_custom_properties.push(new Attribute(component, this, scope, node)); break; } - // fallthrough + // fallthrough case 'Spread': this.attributes.push(new Attribute(component, this, scope, node)); break; @@ -145,9 +147,15 @@ export default class InlineComponent extends Node { info.children.splice(i, 1); } else if (child.type === 'Comment' && children.length > 0) { children[children.length - 1].children.unshift(child); + info.children.splice(i, 1); + } else if (child.type === 'IfBlock' && child.children.some(if_child => if_child.type === 'SlotTemplate')) { + children.push({ + ...child, + type: 'SlotTemplateIfBlock' + }); + info.children.splice(i, 1); } } - if (info.children.some(node => not_whitespace_text(node))) { children.push({ start: info.start, @@ -159,12 +167,18 @@ export default class InlineComponent extends Node { }); } - this.children = map_children(component, this, this.scope, children); + this.children = map_children(component, this, this.scope, children.reverse()); + + this.validate_duplicate_slot_name(); } get slot_template_name() { return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string; } + + validate_duplicate_slot_name() { + validate_get_slot_names(this.children, this.component, this.name); + } } function not_whitespace_text(node) { diff --git a/src/compiler/compile/nodes/SlotTemplate.ts b/src/compiler/compile/nodes/SlotTemplate.ts index 90299c8e39..7b28afb47d 100644 --- a/src/compiler/compile/nodes/SlotTemplate.ts +++ b/src/compiler/compile/nodes/SlotTemplate.ts @@ -66,7 +66,12 @@ export default class SlotTemplate extends Node { } validate_slot_template_placement() { - if (this.parent.type !== 'InlineComponent') { + let parent = this.parent; + while (parent.type === 'SlotTemplateIfBlock' || parent.type === 'SlotTemplateElseBlock') parent = parent.parent; + if (parent.type === 'IfBlock' || parent.type === 'ElseBlock') { + return this.component.error(this, compiler_errors.invalid_mix_element_and_conditional_slot); + } + if (parent.type !== 'InlineComponent') { return this.component.error(this, compiler_errors.invalid_slotted_content_fragment); } } diff --git a/src/compiler/compile/nodes/SlotTemplateElseBlock.ts b/src/compiler/compile/nodes/SlotTemplateElseBlock.ts new file mode 100644 index 0000000000..7b1e23c650 --- /dev/null +++ b/src/compiler/compile/nodes/SlotTemplateElseBlock.ts @@ -0,0 +1,50 @@ +import Component from '../Component'; +import Expression from './shared/Expression'; +import TemplateScope from './shared/TemplateScope'; +import Node from './shared/Node'; +import Let from './Let'; +import Attribute from './Attribute'; +import { INode } from './interfaces'; +import compiler_errors from '../compiler_errors'; +import get_const_tags from './shared/get_const_tags'; +import ConstTag from './ConstTag'; +import AbstractBlock from './shared/AbstractBlock'; +import { regex_only_whitespaces } from '../../utils/patterns'; + + +export default class SlotTemplateElseBlock extends AbstractBlock { + type: 'SlotTemplateElseBlock'; + expression: Expression; + scope: TemplateScope; + const_tags: ConstTag[]; + + constructor( + component: Component, + parent: INode, + scope: TemplateScope, + info: any + ) { + super(component, parent, scope, info); + this.scope = scope.child(); + + const children = []; + for (const child of info.children) { + if (child.type === 'SlotTemplate' || child.type === 'ConstTag') { + children.push(child); + } else if (child.type === 'Comment') { + // ignore + } else if (child.type === 'Text' && regex_only_whitespaces.test(child.data)) { + // ignore + } else if (child.type === 'IfBlock') { + children.push({ + ...child, + type: 'SlotTemplateIfBlock' + }); + } else { + this.component.error(child, compiler_errors.invalid_mix_element_and_conditional_slot); + } + } + + ([this.const_tags, this.children] = get_const_tags(children, component, this, this)); + } +} diff --git a/src/compiler/compile/nodes/SlotTemplateIfBlock.ts b/src/compiler/compile/nodes/SlotTemplateIfBlock.ts new file mode 100644 index 0000000000..40144ad5d6 --- /dev/null +++ b/src/compiler/compile/nodes/SlotTemplateIfBlock.ts @@ -0,0 +1,90 @@ +import SlotTemplateElseBlock from './SlotTemplateElseBlock'; +import Component from '../Component'; +import AbstractBlock from './shared/AbstractBlock'; +import Expression from './shared/Expression'; +import TemplateScope from './shared/TemplateScope'; +import Node from './shared/Node'; +import compiler_errors from '../compiler_errors'; +import get_const_tags from './shared/get_const_tags'; +import { TemplateNode } from '../../interfaces'; +import ConstTag from './ConstTag'; +import { regex_only_whitespaces } from '../../utils/patterns'; +import SlotTemplate from './SlotTemplate'; +import { INode } from './interfaces'; + + +export default class SlotTemplateIfBlock extends AbstractBlock { + type: 'SlotTemplateIfBlock'; + expression: Expression; + else: SlotTemplateElseBlock; + scope: TemplateScope; + const_tags: ConstTag[]; + slot_names = new Set(); + + constructor( + component: Component, + parent: Node, + scope: TemplateScope, + info: TemplateNode + ) { + super(component, parent, scope, info); + this.scope = scope.child(); + + const children = []; + for (const child of info.children) { + if (child.type === 'SlotTemplate' || child.type === 'ConstTag') { + children.push(child); + } else if (child.type === 'Comment') { + // ignore + } else if (child.type === 'Text' && regex_only_whitespaces.test(child.data)) { + // ignore + } else if (child.type === 'IfBlock') { + children.push({ + ...child, + type: 'SlotTemplateIfBlock' + }); + } else { + this.component.error(child, compiler_errors.invalid_mix_element_and_conditional_slot); + } + } + + this.expression = new Expression(component, this, this.scope, info.expression); + ([this.const_tags, this.children] = get_const_tags(children, component, this, this)); + + this.else = info.else + ? new SlotTemplateElseBlock(component, this, scope, { ...info.else, type: 'SlotTemplateElseBlock' }) + : null; + } + + validate_duplicate_slot_name(component_name: string): Map { + const if_slot_names = validate_get_slot_names(this.children, this.component, component_name); + if (!this.else) { + return if_slot_names; + } + + const else_slot_names = validate_get_slot_names(this.else.children, this.component, component_name); + return new Map([...if_slot_names, ...else_slot_names]); + } +} + +export function validate_get_slot_names(children: Array, component: Component, component_name: string) { + const slot_names = new Map(); + function add_slot_name(slot_name: string, child: SlotTemplate) { + if (slot_names.has(slot_name)) { + component.error(child, compiler_errors.duplicate_slot_name_in_component(slot_name, component_name)); + } + slot_names.set(slot_name, child); + } + + for (const child of children) { + if (child.type === 'SlotTemplateIfBlock') { + const child_slot_names = child.validate_duplicate_slot_name(component_name); + for (const [slot_name, child] of child_slot_names) { + add_slot_name(slot_name, child); + } + } else if (child.type === 'SlotTemplate') { + add_slot_name(child.slot_template_name, child); + } + } + return slot_names; +} \ No newline at end of file diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index f023cad25c..44da324c33 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -33,6 +33,8 @@ import ThenBlock from './ThenBlock'; import Title from './Title'; import Transition from './Transition'; import Window from './Window'; +import SlotTemplateIfBlock from './SlotTemplateIfBlock'; +import SlottemplateElseBlock from './SlotTemplateElseBlock'; // note: to write less types each of types in union below should have type defined as literal // https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions @@ -63,6 +65,8 @@ export type INode = Action | RawMustacheTag | Slot | SlotTemplate +| SlotTemplateIfBlock +| SlottemplateElseBlock | StyleDirective | Tag | Text @@ -78,4 +82,6 @@ export type INodeAllowConstTag = | CatchBlock | ThenBlock | InlineComponent -| SlotTemplate; +| SlotTemplate +| SlotTemplateIfBlock +| SlottemplateElseBlock; diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index e6ad1d7f6e..d43b14b9e5 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -19,6 +19,7 @@ import Title from '../Title'; import Window from '../Window'; import { TemplateNode } from '../../../interfaces'; import { push_array } from '../../../utils/push_array'; +import SlotTemplateIfBlock from '../SlotTemplateIfBlock'; export type Children = ReturnType; @@ -40,6 +41,7 @@ function get_constructor(type) { case 'DebugTag': return DebugTag; case 'Slot': return Slot; case 'SlotTemplate': return SlotTemplate; + case 'SlotTemplateIfBlock': return SlotTemplateIfBlock; case 'Text': return Text; case 'Title': return Title; case 'Window': return Window; diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index bc56f2eb27..487869ac3f 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -86,7 +86,7 @@ export default function dom( let compute_slots: Node[] | undefined; if (uses_slots) { compute_slots = b` - const $$slots = @compute_slots(#slots); + let $$slots = @compute_slots(#slots); `; } @@ -114,7 +114,13 @@ export default function dom( b`if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.export_name}`)};` )} ${component.slots.size > 0 && - b`if ('$$scope' in ${$$props}) ${renderer.invalidate('$$scope', x`$$scope = ${$$props}.$$scope`)};`} + b` + if ('$$scope' in ${$$props}) ${renderer.invalidate('$$scope', x`$$scope = ${$$props}.$$scope`)}; + if ('$$slots' in ${$$props}) { + ${renderer.invalidate('#slots', x`#slots = ${$$props}.$$slots`)}; + ${uses_slots ? renderer.invalidate('$$slots', x`$$slots = @compute_slots(#slots)`) : null} + } + `} } ` : null; diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index ab140a75c5..430797766d 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -9,8 +9,6 @@ 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 TemplateScope from '../../../nodes/shared/TemplateScope'; -import is_dynamic from '../shared/is_dynamic'; import bind_this from '../shared/bind_this'; import { Node, Identifier, ObjectExpression } from 'estree'; import EventHandler from '../Element/EventHandler'; @@ -22,17 +20,18 @@ import { is_head } from '../shared/is_head'; import compiler_warnings from '../../../compiler_warnings'; import { namespaces } from '../../../../utils/namespaces'; import { extract_ignores_above_node } from '../../../../utils/extract_svelte_ignore'; - -type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; +import SlotTemplateIfBlockWrapper from '../SlotTemplateIfBlock'; +import SlotTemplateIfBlock from '../../../nodes/SlotTemplateIfBlock'; +import { collect_slot_dynamic_dependencies, collect_slot_fragment_dependencies } from '../shared/slots'; const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g; export default class InlineComponentWrapper extends Wrapper { var: Identifier; - slots: Map = new Map(); node: InlineComponent; fragment: FragmentWrapper; - children: Array = []; + children: Array = []; + has_conditional_slots: boolean = false; constructor( renderer: Renderer, @@ -89,22 +88,19 @@ export default class InlineComponentWrapper extends Wrapper { }); }); - this.children = this.node.children.map(child => new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling)); + for (const child of this.node.children) { + if (child.type === 'SlotTemplate') { + this.children.push(new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling)); + } else if (child.type === 'SlotTemplateIfBlock') { + this.has_conditional_slots = true; + this.children.push(new SlotTemplateIfBlockWrapper(renderer, block, this, child as SlotTemplateIfBlock, 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); @@ -137,25 +133,30 @@ export default class InlineComponentWrapper extends Wrapper { const statements: Array = []; const updates: Array = []; + const name_changes = block.get_unique_name(`${name.name}_changes`); - this.children.forEach((child) => { + const should_cache_slot_definition = this.has_conditional_slots; + for (const slot of this.children) { this.renderer.add_to_context('$$scope', true); - child.render(block, null, x`#nodes` as Identifier); - }); + slot.render_slot_template_content(should_cache_slot_definition); + } + + let get_slots_definition: Node = null; + if (this.has_conditional_slots) { + get_slots_definition = block.renderer.component.get_unique_name(`${name.name}_slots_definition`); + this.renderer.blocks.push(b` + function ${get_slots_definition}(#ctx) { + const #slots_definition = {}; + ${this.children.map(slot => slot.render_slot_template_definition(block))} + return #slots_definition; + } + `); + } let props: Identifier | undefined; - const name_changes = block.get_unique_name(`${name.name}_changes`); const uses_spread = !!this.node.attributes.find(a => a.is_spread); - // removing empty slot - for (const slot of this.slots.keys()) { - if (!this.slots.get(slot).block.has_content()) { - this.renderer.remove_block(this.slots.get(slot).block); - this.slots.delete(slot); - } - } - const has_css_custom_properties = this.node.css_custom_properties.length > 0; const is_svg_namespace = this.node.namespace === namespaces.svg; const css_custom_properties_wrapper_element = is_svg_namespace ? 'g' : 'div'; @@ -164,16 +165,17 @@ export default class InlineComponentWrapper extends Wrapper { block.add_variable(css_custom_properties_wrapper); } - const initial_props = this.slots.size > 0 + const initial_props = this.has_conditional_slots + ? [ + p`$$slots: ${get_slots_definition}(#ctx)`, + p`$$scope: { ctx: #ctx }` + ] + : this.children.length > 0 ? [ p`$$slots: { - ${Array.from(this.slots).map(([name, slot]) => { - return p`${name}: [${slot.block.name}, ${slot.get_context || null}, ${slot.get_changes || null}]`; - })} + ${this.children.map((slot: SlotTemplateWrapper) => p`${slot.slot_template_name}: ${slot.slot_definition}`)} }`, - p`$$scope: { - ctx: #ctx - }` + p`$$scope: { ctx: #ctx }` ] : []; @@ -201,19 +203,12 @@ export default class InlineComponentWrapper extends Wrapper { component_opts.properties.push(p`$$inline: true`); } - 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); - const variable = renderer.component.var_lookup.get(name); - - if (is_let || is_dynamic(variable)) fragment_dependencies.add(name); - }); - }); + const fragment_dependencies = new Set(this.children.length ? ['$$scope'] : []); + collect_slot_fragment_dependencies(renderer, this.children, fragment_dependencies); const dynamic_attributes = this.node.attributes.filter(a => a.get_dependencies().length > 0); - if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0)) { + if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0 || this.has_conditional_slots)) { updates.push(b`const ${name_changes} = {};`); } @@ -309,6 +304,16 @@ export default class InlineComponentWrapper extends Wrapper { }`); } + if (this.has_conditional_slots) { + const dependencies = collect_slot_dynamic_dependencies(this.children); + + updates.push(b` + if (${renderer.dirty(Array.from(dependencies))}) { + ${name_changes}.$$slots = ${get_slots_definition}(#ctx); + } + `) + } + const munged_bindings = this.node.bindings.map(binding => { component.has_reactive_assignments = true; diff --git a/src/compiler/compile/render_dom/wrappers/Slot.ts b/src/compiler/compile/render_dom/wrappers/Slot.ts index 0a589e3394..dacadb593f 100644 --- a/src/compiler/compile/render_dom/wrappers/Slot.ts +++ b/src/compiler/compile/render_dom/wrappers/Slot.ts @@ -125,37 +125,47 @@ export default class SlotWrapper extends Wrapper { } const slot = block.get_unique_name(`${sanitize(slot_name)}_slot`); - const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot_template`); - const slot_or_fallback = has_fallback ? block.get_unique_name(`${sanitize(slot_name)}_slot_or_fallback`) : slot; + const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node(); + const anchor = needs_anchor + ? block.get_unique_name(`${this.var.name}_anchor`) + : (this.next && this.next.var) || x`null`; block.chunks.init.push(b` - const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name}; - const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn}); - ${has_fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null} + ${has_fallback + ? b`const ${slot} = @create_slot_with_fallback(${renderer.context_lookup.get('#slots').index}, '${slot_name}', ${renderer.context_lookup.get('$$scope').index}, #ctx, ${get_slot_context_fn}, ${this.fallback.name});` + : b`const ${slot} = @create_slot(${renderer.context_lookup.get('#slots').index}, '${slot_name}', ${renderer.context_lookup.get('$$scope').index}, #ctx, ${get_slot_context_fn});` + } `); block.chunks.create.push( - b`if (${slot_or_fallback}) ${slot_or_fallback}.c();` + b`${slot}.c();` ); if (renderer.options.hydratable) { block.chunks.claim.push( - b`if (${slot_or_fallback}) ${slot_or_fallback}.l(${parent_nodes});` + b`${slot}.l(${parent_nodes});` ); } block.chunks.mount.push(b` - if (${slot_or_fallback}) { - ${slot_or_fallback}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); - } + ${slot}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); `); + if (needs_anchor) { + block.add_element( + anchor as Identifier, + x`@empty()`, + parent_nodes && x`@empty()`, + parent_node + ); + } + block.chunks.hydrate.push(b`${slot}.a = ${anchor};`); block.chunks.intro.push( - b`@transition_in(${slot_or_fallback}, #local);` + b`@transition_in(${slot}, #local);` ); block.chunks.outro.push( - b`@transition_out(${slot_or_fallback}, #local);` + b`@transition_out(${slot}, #local);` ); const dynamic_dependencies = Array.from(this.dependencies).filter((name) => this.is_dependency_dynamic(name)); @@ -178,17 +188,17 @@ export default class SlotWrapper extends Wrapper { let slot_update: Node[]; if (all_dirty_condition) { - const dirty = x`${all_dirty_condition} ? @get_all_dirty_from_scope(${renderer.reference('$$scope')}) : @get_slot_changes(${slot_definition}, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn})`; + const dirty = x`${all_dirty_condition} ? @get_all_dirty_from_scope(${renderer.reference('$$scope')}) : @get_slot_changes(${slot}.x, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn})`; slot_update = b` - if (${slot}.p && ${condition}) { - @update_slot_base(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${dirty}, ${get_slot_context_fn}); + if (${slot}.s && ${slot}.s.p && ${condition}) { + @update_slot_base(${slot}.s, ${slot}.x, #ctx, ${renderer.reference('$$scope')}, ${dirty}, ${get_slot_context_fn}); } `; } else { slot_update = b` - if (${slot}.p && ${condition}) { - @update_slot(${slot}, ${slot_definition}, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_context_fn}); + if (${slot}.s && ${slot}.s.p && ${condition}) { + @update_slot(${slot}.s, ${slot}.x, #ctx, ${renderer.reference('$$scope')}, #dirty, ${get_slot_changes_fn}, ${get_slot_context_fn}); } `; } @@ -201,29 +211,33 @@ export default class SlotWrapper extends Wrapper { } const fallback_update = has_fallback && fallback_dynamic_dependencies.length > 0 && b` - if (${slot_or_fallback} && ${slot_or_fallback}.p && ${fallback_condition}) { - ${slot_or_fallback}.p(#ctx, ${fallback_dirty}); + if (${slot}.f.p && ${fallback_condition}) { + ${slot}.f.p(#ctx, ${fallback_dirty}); } `; - if (fallback_update) { - block.chunks.update.push(b` - if (${slot}) { + const slot_or_fallback_update = fallback_update + ? b` + if (${slot}.s) { ${slot_update} } else { ${fallback_update} } - `); - } else { - block.chunks.update.push(b` - if (${slot}) { + ` + : b` + if (${slot}.s) { ${slot_update} } - `); - } + `; + + block.chunks.update.push(b` + if (!(${renderer.dirty(['#slots'])} && ${slot}.p(#ctx))) { + ${slot_or_fallback_update} + } + `) block.chunks.destroy.push( - b`if (${slot_or_fallback}) ${slot_or_fallback}.d(detaching);` + b`if (${slot}) ${slot}.d(detaching);` ); } diff --git a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts index a50f74fc04..f1321f1422 100644 --- a/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts +++ b/src/compiler/compile/render_dom/wrappers/SlotTemplate.ts @@ -3,20 +3,25 @@ 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 { get_slot_definition, SlotDefinition } from './shared/get_slot_definition'; import { b, x } from 'code-red'; import { sanitize } from '../../../utils/names'; -import { Identifier } from 'estree'; +import { Identifier, Node } from 'estree'; import InlineComponentWrapper from './InlineComponent'; import { extract_names } from 'periscopic'; import SlotTemplate from '../../nodes/SlotTemplate'; import { add_const_tags, add_const_tags_context } from './shared/add_const_tags'; +import TemplateScope from '../../nodes/shared/TemplateScope'; +import SlotTemplateIfBlockWrapper from './SlotTemplateIfBlock'; export default class SlotTemplateWrapper extends Wrapper { node: SlotTemplate; fragment: FragmentWrapper; block: Block; - parent: InlineComponentWrapper; + parent: InlineComponentWrapper | SlotTemplateIfBlockWrapper; + slot_template_name: string; + slot_definition: Node; + scope: TemplateScope; constructor( renderer: Renderer, @@ -48,14 +53,16 @@ export default class SlotTemplateWrapper extends Wrapper { this.renderer.blocks.push(this.block); const seen = new Set(lets.map(l => l.name.name)); - this.parent.node.lets.forEach(l => { + const component_parent = this.get_component_parent(); + component_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) - ); + const slot_definition = get_slot_definition(this.block, scope, lets); + + this.slot_template_name = slot_template_name; + this.slot_definition = x`[${slot_definition.block.name}, ${slot_definition.get_context || null}, ${slot_definition.get_changes || null}]`; + this.scope = scope; this.fragment = new FragmentWrapper( renderer, @@ -69,13 +76,29 @@ export default class SlotTemplateWrapper extends Wrapper { this.block.parent.add_dependencies(this.block.dependencies); } - render() { + render_slot_template_content(should_cache: boolean) { this.fragment.render(this.block, null, x`#nodes` as Identifier); if (this.node.const_tags.length > 0) { this.render_get_context(); } + + if (!this.block.has_content()) { + this.renderer.remove_block(this.block); + this.slot_definition = null; + } + + if (this.slot_definition && should_cache) { + const cached_name = this.renderer.component.get_unique_name(`${this.slot_template_name}_definition`); + this.renderer.blocks.push(b`var ${cached_name} = ${this.slot_definition};`); + this.slot_definition = cached_name; + } } + + render_slot_template_definition(_block: Block) { + return b`#slots_definition["${this.slot_template_name}"] = ${this.slot_definition};`; + } + render_get_context() { const get_context = this.block.renderer.component.get_unique_name('get_context'); this.block.renderer.blocks.push(b` @@ -88,4 +111,10 @@ export default class SlotTemplateWrapper extends Wrapper { this.block.chunks.update.unshift(b`${get_context}(#ctx)`); } } + + get_component_parent() { + let parent: Wrapper = this.parent; + while (parent.node.type !== 'InlineComponent') parent = parent.parent; + return parent as InlineComponentWrapper; + } } diff --git a/src/compiler/compile/render_dom/wrappers/SlotTemplateIfBlock.ts b/src/compiler/compile/render_dom/wrappers/SlotTemplateIfBlock.ts new file mode 100644 index 0000000000..9915aad3f4 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/SlotTemplateIfBlock.ts @@ -0,0 +1,74 @@ +import Wrapper from './shared/Wrapper'; +import Renderer from '../Renderer'; +import Block from '../Block'; +import { Identifier } from 'estree'; +import SlotTemplateIfBlock from '../../nodes/SlotTemplateIfBlock'; +import SlotTemplateWrapper from './SlotTemplate'; +import SlotTemplate from '../../nodes/SlotTemplate'; +import { b } from 'code-red'; +import InlineComponentWrapper from './InlineComponent'; +import TemplateScope from '../../nodes/shared/TemplateScope'; + +export default class SlotTemplateIfBlockWrapper extends Wrapper { + node: SlotTemplateIfBlock; + needs_update = false; + + var: Identifier = { type: 'Identifier', name: 'if_block' }; + children: Array = []; + else: Array = []; + parent: SlotTemplateIfBlockWrapper | InlineComponentWrapper; + scope: TemplateScope + + constructor( + renderer: Renderer, + block: Block, + parent: Wrapper, + node: SlotTemplateIfBlock, + strip_whitespace: boolean, + next_sibling: Wrapper + ) { + super(renderer, block, parent, node); + this.scope = node.scope; + + for (const child of this.node.children) { + if (child.type === 'SlotTemplate') { + this.children.push(new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling)); + } else if (child.type === 'SlotTemplateIfBlock') { + this.children.push(new SlotTemplateIfBlockWrapper(renderer, block, this, child as SlotTemplateIfBlock, strip_whitespace, next_sibling)); + } + } + + if (node.else) { + for (const child of node.else.children) { + if (child.type === 'SlotTemplate') { + this.else.push(new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling)); + } else if (child.type === 'SlotTemplateIfBlock') { + this.else.push(new SlotTemplateIfBlockWrapper(renderer, block, this, child as SlotTemplateIfBlock, strip_whitespace, next_sibling)); + } + } + } + } + + render_slot_template_content(should_cache: boolean) { + this.children.forEach(slot => slot.render_slot_template_content(should_cache)); + this.else.forEach(slot => slot.render_slot_template_content(should_cache)); + } + + render_slot_template_definition(block: Block) { + if (this.else.length > 0) { + return b` + if (${this.node.expression.manipulate(block, '#ctx')}) { + ${this.children.map(slot => slot.render_slot_template_definition(block))} + } else { + ${this.else.map(slot => slot.render_slot_template_definition(block))} + } + `; + } + + return b` + if (${this.node.expression.manipulate(block, '#ctx')}) { + ${this.children.map(slot => slot.render_slot_template_definition(block))} + } + `; + } +} diff --git a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts index 2114096988..531e6397ed 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/get_slot_definition.ts @@ -2,9 +2,11 @@ import Let from '../../../nodes/Let'; import { x, p } from 'code-red'; import Block from '../../Block'; import TemplateScope from '../../../nodes/shared/TemplateScope'; -import { BinaryExpression, Identifier } from 'estree'; +import { BinaryExpression, Identifier, Node } from 'estree'; -export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]) { +export type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; + +export function get_slot_definition(block: Block, scope: TemplateScope, lets: Let[]): SlotDefinition { if (lets.length === 0) return { block, scope }; const context_input = { diff --git a/src/compiler/compile/render_dom/wrappers/shared/slots.ts b/src/compiler/compile/render_dom/wrappers/shared/slots.ts new file mode 100644 index 0000000000..94003e5c8b --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/slots.ts @@ -0,0 +1,52 @@ +import Renderer from "../../Renderer"; +import SlotTemplateWrapper from "../SlotTemplate"; +import SlotTemplateIfBlockWrapper from "../SlotTemplateIfBlock"; +import is_dynamic from "./is_dynamic"; + +export function collect_slot_fragment_dependencies( + renderer: Renderer, + children: Array, + fragment_dependencies: Set +) { + function collect( + children: Array + ) { + for (const child of children) { + if (child instanceof SlotTemplateIfBlockWrapper) { + collect(child.children); + collect(child.else); + for (const dep of child.node.expression.dependencies) { + const is_let = child.scope.is_let(dep); + const variable = renderer.component.var_lookup.get(dep); + if (is_let || is_dynamic(variable)) fragment_dependencies.add(dep); + } + } else { + for (const dep of child.block.dependencies) { + const is_let = child.scope.is_let(dep); + const variable = renderer.component.var_lookup.get(dep); + if (is_let || is_dynamic(variable)) fragment_dependencies.add(dep); + } + } + } + } + collect(children); +} + +export function collect_slot_dynamic_dependencies(children: Array) { + const result = new Set(); + + function collect(children: Array) { + for (const child of children) { + if (child instanceof SlotTemplateIfBlockWrapper) { + for (const dep of child.node.expression.dynamic_dependencies()) { + result.add(dep); + } + collect(child.children); + collect(child.else); + } + } + } + collect(children); + + return result; +} \ No newline at end of file diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 8adb0c279b..d46495bba4 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -1,6 +1,7 @@ import { Readable } from 'svelte/store'; +import { check_outros, group_outros, transition_in, transition_out } from './transitions'; -export function noop() {} +export function noop() { } export const identity = x => x; @@ -84,11 +85,109 @@ export function component_subscribe(component, store, callback) { component.$$.on_destroy.push(subscribe(store, callback)); } -export function create_slot(definition, ctx, $$scope, fn) { - if (definition) { - const slot_ctx = get_slot_context(definition, ctx, $$scope, fn); - return definition[0](slot_ctx); +export function create_slot(definition_index: number, definition_name: string, $$scope_index: number, ctx, get_slot_context_fn) { + let definition; + let slot_block; + function init() { + slot.x = definition = ctx[definition_index][definition_name]; + const $$scope = ctx[$$scope_index]; + if (definition) { + const slot_ctx = get_slot_context(definition, ctx, $$scope, get_slot_context_fn); + slot.s = slot_block = definition[0](slot_ctx); + } else { + slot.s = slot_block = null; + } + } + let _target; + + const slot = { + a: null, + s: null, + f: null, + x: definition, + c: () => slot_block && slot_block.c(), + m: (target, anchor) => { + _target = target; + slot_block && slot_block.m(target, anchor); + }, + p: (ctx) => { + if ((definition !== (definition = ctx[definition_index][definition_name]))) { + if (slot_block) { + if (slot_block.o) { + group_outros(); + transition_out(slot_block, 1, 1, () => {}); + check_outros(); + } else { + slot_block.d(1); + } + } + init(); + if (slot_block) { + slot_block.c(); + transition_in(slot_block, 1); + slot_block.m(_target, slot.a); + } + return true; + } + }, + i: (local) => transition_in(slot_block, local), + o: (local) => transition_out(slot_block, local), + d: (detaching) => slot_block && slot_block.d(detaching), } + + init(); + return slot; +} + +export function create_slot_with_fallback(definition_index: number, definition_name: string, $$scope_index: number, ctx, get_slot_context_fn, fallback) { + let definition; + let slot_or_fallback; + function init() { + slot.x = definition = ctx[definition_index][definition_name]; + const $$scope = ctx[$$scope_index]; + if (definition) { + const slot_ctx = get_slot_context(definition, ctx, $$scope, get_slot_context_fn); + slot.s = slot_or_fallback = definition[0](slot_ctx); + slot.f = null; + } else { + slot.s = null; + slot.f = slot_or_fallback = fallback(ctx); + } + } + let _target; + + const slot = { + a: null, + s: null, + f: null, + x: definition, + c: () => slot_or_fallback.c(), + m: (target, anchor) => { + slot_or_fallback.m(_target = target, anchor); + }, + p: (ctx) => { + if ((definition !== (definition = ctx[definition_index][definition_name]))) { + if (slot_or_fallback.o) { + group_outros(); + transition_out(slot_or_fallback, 1, 1, () => {}); + check_outros(); + } else { + slot_or_fallback.d(1); + } + init(); + slot_or_fallback.c(); + transition_in(slot_or_fallback, 1); + slot_or_fallback.m(_target, slot.a); + return true; + } + }, + i: (local) => transition_in(slot_or_fallback, local), + o: (local) => transition_out(slot_or_fallback, local), + d: (detaching) => slot_or_fallback.d(detaching), + } + + init(); + return slot; } function get_slot_context(definition, ctx, $$scope, fn) { @@ -168,7 +267,7 @@ export function compute_slots(slots) { export function once(fn) { let ran = false; - return function(this: any, ...args) { + return function (this: any, ...args) { if (ran) return; ran = true; fn.call(this, ...args); diff --git a/test/helpers.ts b/test/helpers.ts index 1ea0b19880..d263541d02 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -207,6 +207,7 @@ export function addLineNumbers(code) { } export function showOutput(cwd, options = {}, compile = svelte.compile) { + return; glob('**/*.svelte', { cwd }).forEach(file => { if (file[0] === '_') return; diff --git a/test/runtime/samples/component-dynamic-slot-1/Nested.svelte b/test/runtime/samples/component-dynamic-slot-1/Nested.svelte new file mode 100644 index 0000000000..e53bc9e0c2 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-1/Nested.svelte @@ -0,0 +1 @@ +

Fallback

\ No newline at end of file diff --git a/test/runtime/samples/component-dynamic-slot-1/_config.js b/test/runtime/samples/component-dynamic-slot-1/_config.js new file mode 100644 index 0000000000..74dfd060bf --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-1/_config.js @@ -0,0 +1,27 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: '

Fallback

', + test({ assert, component, target }) { + component.value = 1; + assert.htmlEqual(target.innerHTML, ` +

One

+ `); + + component.value = 2; + assert.htmlEqual(target.innerHTML, ` +

Two

+ `); + + component.value = 3; + assert.htmlEqual(target.innerHTML, ` +

Fallback

+ `); + + component.value = 4; + assert.htmlEqual(target.innerHTML, ` +

Fallback

+ `); + } +}; diff --git a/test/runtime/samples/component-dynamic-slot-1/main.svelte b/test/runtime/samples/component-dynamic-slot-1/main.svelte new file mode 100644 index 0000000000..10f636572e --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-1/main.svelte @@ -0,0 +1,12 @@ + + + + {#if value === 1} + One + {:else if value === 2} + Two + {/if} + diff --git a/test/runtime/samples/component-dynamic-slot-2/Nested.svelte b/test/runtime/samples/component-dynamic-slot-2/Nested.svelte new file mode 100644 index 0000000000..8a317301bb --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-2/Nested.svelte @@ -0,0 +1,15 @@ +
Slot A
+ + + +
Slot B
+ +
Fallback B
+ +
Slot C
+ +
Fallback C
+ +
Slot D
+ + \ No newline at end of file diff --git a/test/runtime/samples/component-dynamic-slot-2/_config.js b/test/runtime/samples/component-dynamic-slot-2/_config.js new file mode 100644 index 0000000000..c297da11a2 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-2/_config.js @@ -0,0 +1,81 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: ` +
Slot A
+ 4A +
Slot B
+ 4B +
Slot C
+
Fallback C
+
Slot D
+ `, + test({ assert, component, target }) { + component.value = 1; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+ A +
Slot B
+
Fallback B
+
Slot C
+
Fallback C
+
Slot D
+ `); + + component.value = 2; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+ 2A +
Slot B
+ 2B +
Slot C
+ 2C +
Slot D
+ `); + + component.value = 3; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+ 3A +
Slot B
+ 3B +
Slot C
+
Fallback C
+
Slot D
+ `); + + component.condition = false; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+ 3A +
Slot B
+
Fallback B
+
Slot C
+
Fallback C
+
Slot D
+ 3D + `); + + component.value = 4; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+
Slot B
+
Fallback B
+
Slot C
+
Fallback C
+
Slot D
+ `); + + component.value = 5; + assert.htmlEqual(target.innerHTML, ` +
Slot A
+ 5A +
Slot B
+
Fallback B
+
Slot C
+
Fallback C
+
Slot D
+ `); + } +}; diff --git a/test/runtime/samples/component-dynamic-slot-2/main.svelte b/test/runtime/samples/component-dynamic-slot-2/main.svelte new file mode 100644 index 0000000000..a03c151390 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-2/main.svelte @@ -0,0 +1,31 @@ + + + + {#if value === 1} + A + {:else if value === 2} + 2A + 2B + 2C + {:else if value === 3} + 3A + {#if condition} + 3B + {:else} + 3D + {/if} + {:else} + {#if condition} + 4A + 4B + {:else} + {#if value === 5} + 5A + {/if} + {/if} + {/if} + diff --git a/test/runtime/samples/component-dynamic-slot-3/Nested.svelte b/test/runtime/samples/component-dynamic-slot-3/Nested.svelte new file mode 100644 index 0000000000..0101a62140 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-3/Nested.svelte @@ -0,0 +1,16 @@ + + +
+ value: {value} +
+ + fallback folder + +
+ + fallback file + +
+ diff --git a/test/runtime/samples/component-dynamic-slot-3/_config.js b/test/runtime/samples/component-dynamic-slot-3/_config.js new file mode 100644 index 0000000000..0ff43f5998 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-3/_config.js @@ -0,0 +1,81 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: ` +
+ value: _ +
+
+ value: a +
+
+ value: b +
+ fallback folder +
+
#2 level
+
+
+ fallback file +
+
+ fallback file +
+ `, + test({ assert, component, target }) { + const lvl1 = target.querySelector('#a'); + const lvl2 = target.querySelector('#b'); + component.paths = ["x", "y", "z"]; + assert.htmlEqual(target.innerHTML, ` +
+ value: _ +
+
+ value: x +
+
+ value: y +
+
+ value: z +
+ fallback folder +
+
#3 level
+
+
+ fallback file +
+
+ fallback file +
+
+ fallback file +
+ `); + + assert.equal(lvl1, target.querySelector('#x')); + assert.equal(lvl2, target.querySelector('#y')); + const lvl3 = target.querySelector('#z'); + + component.paths = ["p"]; + assert.htmlEqual(target.innerHTML, ` +
+ value: _ +
+
+ value: p +
+ fallback folder +
+
#1 level
+
+
+ fallback file +
+ `); + + assert.equal(lvl1, target.querySelector('#p')); + } +}; diff --git a/test/runtime/samples/component-dynamic-slot-3/main.svelte b/test/runtime/samples/component-dynamic-slot-3/main.svelte new file mode 100644 index 0000000000..f92ce7c287 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-3/main.svelte @@ -0,0 +1,46 @@ + + + + {#if paths[0]} + + + {#if paths[1]} + + + {#if paths[2]} + + + {#if paths[3]} + + + + {:else} + +
#3 level
+
+ {/if} +
+
+ {:else} + +
#2 level
+
+ {/if} +
+
+ {:else} + +
#1 level
+
+ {/if} +
+
+ {:else} + +
#0 level
+
+ {/if} +
diff --git a/test/runtime/samples/component-dynamic-slot-4/Nested.svelte b/test/runtime/samples/component-dynamic-slot-4/Nested.svelte new file mode 100644 index 0000000000..4ad905aa7e --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-4/Nested.svelte @@ -0,0 +1,7 @@ + + +{value} \ No newline at end of file diff --git a/test/runtime/samples/component-dynamic-slot-4/_config.js b/test/runtime/samples/component-dynamic-slot-4/_config.js new file mode 100644 index 0000000000..0bc73b00a0 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-4/_config.js @@ -0,0 +1,30 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: `a`, + test({ assert, component, target }) { + component.a = 'foo'; + assert.htmlEqual(target.innerHTML, 'foo'); + + component.condition = 2; + assert.htmlEqual(target.innerHTML, 'foo + b = foob'); + + component.b = 'bar'; + assert.htmlEqual(target.innerHTML, 'foo + bar = foobar'); + + component.condition = 3; + assert.htmlEqual(target.innerHTML, 'b: bar'); + + component.condition = 4; + assert.htmlEqual(target.innerHTML, 'value'); + + component.value = 'xxx'; + assert.htmlEqual(target.innerHTML, 'xxx'); + + component.condition = 2; + component.b = 'baz'; + component.a = 'qux'; + assert.htmlEqual(target.innerHTML, 'qux + baz = quxbaz'); + } +}; diff --git a/test/runtime/samples/component-dynamic-slot-4/main.svelte b/test/runtime/samples/component-dynamic-slot-4/main.svelte new file mode 100644 index 0000000000..a065c872dd --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-4/main.svelte @@ -0,0 +1,23 @@ + + + + {#if condition === 1} + + {a} + + {:else if condition === 2} + + {a} + {b} = {a + b} + + {:else if condition === 3} + + b: {b} + + {/if} + diff --git a/test/runtime/samples/component-dynamic-slot-5/Bar.svelte b/test/runtime/samples/component-dynamic-slot-5/Bar.svelte new file mode 100644 index 0000000000..b4202403fa --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-5/Bar.svelte @@ -0,0 +1,5 @@ +top fallback +
+middle fallback +
+bottom fallback \ No newline at end of file diff --git a/test/runtime/samples/component-dynamic-slot-5/Foo.svelte b/test/runtime/samples/component-dynamic-slot-5/Foo.svelte new file mode 100644 index 0000000000..41d109ae56 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-5/Foo.svelte @@ -0,0 +1,13 @@ + + + + {#if $$slots.top} + + {/if} + Middle + {#if $$slots.bottom} + + {/if} + diff --git a/test/runtime/samples/component-dynamic-slot-5/_config.js b/test/runtime/samples/component-dynamic-slot-5/_config.js new file mode 100644 index 0000000000..f20d52eca8 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-5/_config.js @@ -0,0 +1,28 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: ` +
+ Top content +
+ Middle +
+ bottom fallback +
+
+ Top content +
+ Middle +
+ Bottom content +
+
+ top fallback +
+ Middle Content +
+ bottom fallback +
+ `, +}; diff --git a/test/runtime/samples/component-dynamic-slot-5/main.svelte b/test/runtime/samples/component-dynamic-slot-5/main.svelte new file mode 100644 index 0000000000..9f3d3871d3 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-5/main.svelte @@ -0,0 +1,22 @@ + + +
+ + Top content + +
+ +
+ + Top content + Bottom content + +
+ +
+ + Middle Content + +
diff --git a/test/runtime/samples/component-dynamic-slot-6/Bar.svelte b/test/runtime/samples/component-dynamic-slot-6/Bar.svelte new file mode 100644 index 0000000000..b4202403fa --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-6/Bar.svelte @@ -0,0 +1,5 @@ +top fallback +
+middle fallback +
+bottom fallback \ No newline at end of file diff --git a/test/runtime/samples/component-dynamic-slot-6/Foo.svelte b/test/runtime/samples/component-dynamic-slot-6/Foo.svelte new file mode 100644 index 0000000000..41d109ae56 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-6/Foo.svelte @@ -0,0 +1,13 @@ + + + + {#if $$slots.top} + + {/if} + Middle + {#if $$slots.bottom} + + {/if} + diff --git a/test/runtime/samples/component-dynamic-slot-6/_config.js b/test/runtime/samples/component-dynamic-slot-6/_config.js new file mode 100644 index 0000000000..eb11146fe5 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-6/_config.js @@ -0,0 +1,33 @@ +export default { + solo: true, + skip_if_ssr: true, + skip_if_hydrate: true, + html: ` + top fallback +
+ Middle +
+ bottom fallback + `, + test({ assert, component, target }) { + component.top = true; + assert.htmlEqual(target.innerHTML, ` + Top content +
+ Middle +
+ bottom fallback + `); + + component.top = false; + component.middle = true; + component.bottom = true; + assert.htmlEqual(target.innerHTML, ` + top fallback +
+ Middle content +
+ Bottom content + `); + } +}; diff --git a/test/runtime/samples/component-dynamic-slot-6/main.svelte b/test/runtime/samples/component-dynamic-slot-6/main.svelte new file mode 100644 index 0000000000..77bbe16ce2 --- /dev/null +++ b/test/runtime/samples/component-dynamic-slot-6/main.svelte @@ -0,0 +1,18 @@ + + + + {#if top} + Top content + {/if} + {#if middle} + Middle content + {/if} + {#if bottom} + Bottom content + {/if} + diff --git a/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-2/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error-2/_config.js b/test/runtime/samples/component-slot-duplicate-error-2/_config.js deleted file mode 100644 index 1c9fa51dc3..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-2/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - error: 'Duplicate slot name "foo" in ' -}; diff --git a/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-3/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error-3/_config.js b/test/runtime/samples/component-slot-duplicate-error-3/_config.js deleted file mode 100644 index 1c9fa51dc3..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-3/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - error: 'Duplicate slot name "foo" in ' -}; diff --git a/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte deleted file mode 100644 index 0385342cef..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-4/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error-4/_config.js b/test/runtime/samples/component-slot-duplicate-error-4/_config.js deleted file mode 100644 index cfe8e7054e..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error-4/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - error: 'Found elements without slot attribute when using slot="default"' -}; diff --git a/test/runtime/samples/component-slot-duplicate-error/Nested.svelte b/test/runtime/samples/component-slot-duplicate-error/Nested.svelte deleted file mode 100644 index 32eee1534a..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error/Nested.svelte +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/component-slot-duplicate-error/_config.js b/test/runtime/samples/component-slot-duplicate-error/_config.js deleted file mode 100644 index 1c9fa51dc3..0000000000 --- a/test/runtime/samples/component-slot-duplicate-error/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - error: 'Duplicate slot name "foo" in ' -}; diff --git a/test/validator/samples/component-slot-duplicate-1/errors.json b/test/validator/samples/component-slot-duplicate-1/errors.json new file mode 100644 index 0000000000..c7310ac55a --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-1/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"foo\" in ", + "start": { "line": 7, "column": 1, "character": 96 }, + "end": { "line": 7, "column": 26, "character": 121 }, + "pos": 96 + } +] diff --git a/test/runtime/samples/component-slot-duplicate-error/main.svelte b/test/validator/samples/component-slot-duplicate-1/input.svelte similarity index 100% rename from test/runtime/samples/component-slot-duplicate-error/main.svelte rename to test/validator/samples/component-slot-duplicate-1/input.svelte diff --git a/test/validator/samples/component-slot-duplicate-10/errors.json b/test/validator/samples/component-slot-duplicate-10/errors.json new file mode 100644 index 0000000000..60a7b0ca11 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-10/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"a\" in ", + "start": { "line": 11, "column": 2, "character": 179 }, + "end": { "line": 13, "column": 20, "character": 245 }, + "pos": 179 + } +] diff --git a/test/validator/samples/component-slot-duplicate-10/input.svelte b/test/validator/samples/component-slot-duplicate-10/input.svelte new file mode 100644 index 0000000000..3ab2636661 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-10/input.svelte @@ -0,0 +1,15 @@ + + + + {#if condition} + +
test
+
+ +
test
+
+ {/if} +
\ No newline at end of file diff --git a/test/validator/samples/component-slot-duplicate-2/errors.json b/test/validator/samples/component-slot-duplicate-2/errors.json new file mode 100644 index 0000000000..5e06332162 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-2/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"foo\" in ", + "start": { "line": 7, "column": 1, "character": 124 }, + "end": { "line": 7, "column": 54, "character": 177 }, + "pos": 124 + } +] diff --git a/test/runtime/samples/component-slot-duplicate-error-2/main.svelte b/test/validator/samples/component-slot-duplicate-2/input.svelte similarity index 100% rename from test/runtime/samples/component-slot-duplicate-error-2/main.svelte rename to test/validator/samples/component-slot-duplicate-2/input.svelte diff --git a/test/validator/samples/component-slot-duplicate-3/errors.json b/test/validator/samples/component-slot-duplicate-3/errors.json new file mode 100644 index 0000000000..67892b4f26 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-3/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"foo\" in ", + "start": { "line": 7, "column": 1, "character": 124 }, + "end": { "line": 7, "column": 26, "character": 149 }, + "pos": 124 + } +] diff --git a/test/runtime/samples/component-slot-duplicate-error-3/main.svelte b/test/validator/samples/component-slot-duplicate-3/input.svelte similarity index 100% rename from test/runtime/samples/component-slot-duplicate-error-3/main.svelte rename to test/validator/samples/component-slot-duplicate-3/input.svelte diff --git a/test/validator/samples/component-slot-duplicate-4/errors.json b/test/validator/samples/component-slot-duplicate-4/errors.json new file mode 100644 index 0000000000..4b5c380f8e --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-4/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Found elements without slot attribute when using slot=\"default\"", + "start": { "line": 6, "column": 1, "character": 69 }, + "end": { "line": 6, "column": 56, "character": 124 }, + "pos": 69 + } +] diff --git a/test/runtime/samples/component-slot-duplicate-error-4/main.svelte b/test/validator/samples/component-slot-duplicate-4/input.svelte similarity index 100% rename from test/runtime/samples/component-slot-duplicate-error-4/main.svelte rename to test/validator/samples/component-slot-duplicate-4/input.svelte diff --git a/test/validator/samples/component-slot-duplicate-5/errors.json b/test/validator/samples/component-slot-duplicate-5/errors.json new file mode 100644 index 0000000000..b42103ae46 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-5/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Found elements without slot attribute when using slot=\"default\"", + "start": { "line": 11, "column": 2, "character": 154 }, + "end": { "line": 13, "column": 20, "character": 226 }, + "pos": 154 + } +] diff --git a/test/validator/samples/component-slot-duplicate-5/input.svelte b/test/validator/samples/component-slot-duplicate-5/input.svelte new file mode 100644 index 0000000000..6547516fcc --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-5/input.svelte @@ -0,0 +1,15 @@ + + + + {#if condition} + test + {/if} + {#if condition} + +
test
+
+ {/if} +
\ No newline at end of file diff --git a/test/validator/samples/component-slot-duplicate-6/errors.json b/test/validator/samples/component-slot-duplicate-6/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-6/errors.json @@ -0,0 +1 @@ +[] diff --git a/test/validator/samples/component-slot-duplicate-6/input.svelte b/test/validator/samples/component-slot-duplicate-6/input.svelte new file mode 100644 index 0000000000..50926409e6 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-6/input.svelte @@ -0,0 +1,66 @@ + + + + {#if condition} + +
test
+
+ {:else} + +
test
+
+ {/if} + + {#if condition} + +
test
+
+ {:else if condition} + +
test
+
+ {/if} + + {#if condition} + +
test
+
+ {:else} + {#if condition} + +
test
+
+ {:else} + +
test
+
+ {/if} + {/if} + + {#if condition} + +
test
+
+ {:else} + {#if condition} + {#if condition} + {#if condition} + +
test
+
+ {/if} + {/if} + {:else} + {#if condition} + {#if condition} + +
test
+
+ {/if} + {/if} + {/if} + {/if} +
\ No newline at end of file diff --git a/test/validator/samples/component-slot-duplicate-7/errors.json b/test/validator/samples/component-slot-duplicate-7/errors.json new file mode 100644 index 0000000000..ee611caae8 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-7/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"a\" in ", + "start": { "line": 20, "column": 5, "character": 333 }, + "end": { "line": 22, "column": 23, "character": 405 }, + "pos": 333 + } +] diff --git a/test/validator/samples/component-slot-duplicate-7/input.svelte b/test/validator/samples/component-slot-duplicate-7/input.svelte new file mode 100644 index 0000000000..cc620d1029 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-7/input.svelte @@ -0,0 +1,31 @@ + + + + {#if condition} + +
test
+
+ {/if} + + {#if condition} + +
test
+
+ {#if condition} + {#if condition} + {#if condition} + +
test
+
+ {/if} + {/if} + {/if} + {:else if condition} + +
test
+
+ {/if} +
diff --git a/test/validator/samples/component-slot-duplicate-8/errors.json b/test/validator/samples/component-slot-duplicate-8/errors.json new file mode 100644 index 0000000000..2c055f14f0 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-8/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"a\" in ", + "start": { "line": 24, "column": 5, "character": 424 }, + "end": { "line": 26, "column": 23, "character": 496 }, + "pos": 424 + } +] diff --git a/test/validator/samples/component-slot-duplicate-8/input.svelte b/test/validator/samples/component-slot-duplicate-8/input.svelte new file mode 100644 index 0000000000..45f934cc7c --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-8/input.svelte @@ -0,0 +1,31 @@ + + + + {#if condition} + +
test
+
+ {/if} + + {#if condition} + +
test
+
+ {:else if condition} + +
test
+
+ {#if condition} + {#if condition} + {#if condition} + +
test
+
+ {/if} + {/if} + {/if} + {/if} +
diff --git a/test/validator/samples/component-slot-duplicate-9/errors.json b/test/validator/samples/component-slot-duplicate-9/errors.json new file mode 100644 index 0000000000..05f209add8 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-9/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "duplicate-slot-name-in-component", + "message": "Duplicate slot name \"d\" in ", + "start": { "line": 13, "column": 1, "character": 195 }, + "end": { "line": 15, "column": 19, "character": 259 }, + "pos": 195 + } +] diff --git a/test/validator/samples/component-slot-duplicate-9/input.svelte b/test/validator/samples/component-slot-duplicate-9/input.svelte new file mode 100644 index 0000000000..cd821a69a6 --- /dev/null +++ b/test/validator/samples/component-slot-duplicate-9/input.svelte @@ -0,0 +1,16 @@ + + + + {#if condition} + +
test
+
+ {/if} + + +
test
+
+
diff --git a/test/validator/samples/conditional-slot-mix-element-2/errors.json b/test/validator/samples/conditional-slot-mix-element-2/errors.json new file mode 100644 index 0000000000..bfc971fdfb --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-2/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-mix-element-and-conditional-slot", + "message": "Do not mix and other elements under the same {#if}{:else} group. Default slot content should be wrapped with ", + "start": { "line": 10, "column": 2, "character": 139 }, + "end": { "line": 12, "column": 20, "character": 196 }, + "pos": 139 + } +] diff --git a/test/validator/samples/conditional-slot-mix-element-2/input.svelte b/test/validator/samples/conditional-slot-mix-element-2/input.svelte new file mode 100644 index 0000000000..efb0c08b5b --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-2/input.svelte @@ -0,0 +1,14 @@ + + + + {#if condition} + test + {:else} + +
test
+
+ {/if} +
\ No newline at end of file diff --git a/test/validator/samples/conditional-slot-mix-element-3/errors.json b/test/validator/samples/conditional-slot-mix-element-3/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-3/errors.json @@ -0,0 +1 @@ +[] diff --git a/test/validator/samples/conditional-slot-mix-element-3/input.svelte b/test/validator/samples/conditional-slot-mix-element-3/input.svelte new file mode 100644 index 0000000000..dc5c0a9d67 --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-3/input.svelte @@ -0,0 +1,18 @@ + + + + {#if condition} + test + {/if} + {#if condition} + +
test
+
+ +
test
+
+ {/if} +
\ No newline at end of file diff --git a/test/validator/samples/conditional-slot-mix-element-4/errors.json b/test/validator/samples/conditional-slot-mix-element-4/errors.json new file mode 100644 index 0000000000..e41b7c868f --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-4/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-mix-element-and-conditional-slot", + "message": "Do not mix and other elements under the same {#if}{:else} group. Default slot content should be wrapped with ", + "start": { "line": 9, "column": 6, "character": 124 }, + "end": { "line": 9, "column": 36, "character": 154 }, + "pos": 124 + } +] diff --git a/test/validator/samples/conditional-slot-mix-element-4/input.svelte b/test/validator/samples/conditional-slot-mix-element-4/input.svelte new file mode 100644 index 0000000000..d1179da0fe --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element-4/input.svelte @@ -0,0 +1,12 @@ + + + +
+ {#if condition} + + {/if} +
+
\ No newline at end of file diff --git a/test/validator/samples/conditional-slot-mix-element/errors.json b/test/validator/samples/conditional-slot-mix-element/errors.json new file mode 100644 index 0000000000..030ae4d083 --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element/errors.json @@ -0,0 +1,9 @@ +[ + { + "code": "invalid-mix-element-and-conditional-slot", + "message": "Do not mix and other elements under the same {#if}{:else} group. Default slot content should be wrapped with ", + "start": { "line": 11, "column": 2, "character": 170 }, + "end": { "line": 11, "column": 19, "character": 187 }, + "pos": 170 + } +] diff --git a/test/validator/samples/conditional-slot-mix-element/input.svelte b/test/validator/samples/conditional-slot-mix-element/input.svelte new file mode 100644 index 0000000000..2e1ddf5bb4 --- /dev/null +++ b/test/validator/samples/conditional-slot-mix-element/input.svelte @@ -0,0 +1,13 @@ + + + + {#if condition} + +
test
+
+ test + {/if} +