diff --git a/src/compile/Component.ts b/src/compile/Component.ts index f1bf634786..bf2fdf9a5a 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -20,6 +20,7 @@ import fuzzymatch from '../utils/fuzzymatch'; import { remove_indentation, add_indentation } from '../utils/indentation'; import get_object from './utils/get_object'; import unwrap_parens from './utils/unwrap_parens'; +import Slot from './nodes/Slot'; type ComponentOptions = { namespace?: string; @@ -117,6 +118,9 @@ export default class Component { used_names: Set = new Set(); globally_used_names: Set = new Set(); + slots: Map = new Map(); + slot_outlets: Set = new Set(); + constructor( ast: Ast, source: string, diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index bc1991c57e..b38ee5b6fb 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -361,6 +361,15 @@ export default class Element extends Node { }); } + if (component.slot_outlets.has(name)) { + component.error(attribute, { + code: `duplicate-slot-attribute`, + message: `Duplicate '${name}' slot` + }); + + component.slot_outlets.add(name); + } + let ancestor = this.parent; do { if (ancestor.type === 'InlineComponent') break; diff --git a/src/compile/nodes/Slot.ts b/src/compile/nodes/Slot.ts index 1540fa6271..dbb502b41a 100644 --- a/src/compile/nodes/Slot.ts +++ b/src/compile/nodes/Slot.ts @@ -1,15 +1,17 @@ import Node from './shared/Node'; import Element from './Element'; import Attribute from './Attribute'; +import Component from '../Component'; +import TemplateScope from './shared/TemplateScope'; export default class Slot extends Element { type: 'Element'; name: string; - slot_name: string; - attributes: Attribute[]; children: Node[]; + slot_name: string; + values: Map = new Map(); - constructor(component, parent, scope, info) { + constructor(component: Component, parent: Node, scope: TemplateScope, info: any) { super(component, parent, scope, info); info.attributes.forEach(attr => { @@ -37,23 +39,33 @@ export default class Slot extends Element { } } - // TODO should duplicate slots be disallowed? Feels like it's more likely to be a - // bug than anything. Perhaps it should be a warning - - // if (validator.slots.has(slot_name)) { - // validator.error(`duplicate '${slot_name}' element`, nameAttribute.start); - // } - - // validator.slots.add(slot_name); + this.values.set(attr.name, new Attribute(component, this, scope, attr)); }); if (!this.slot_name) this.slot_name = 'default'; - // if (node.attributes.length === 0) && validator.slots.has('default')) { - // validator.error(node, { - // code: `duplicate-slot`, - // message: `duplicate default element` - // }); - // } + if (this.slot_name === 'default') { + // if this is the default slot, add our dependencies to any + // other slots (which inherit our slot values) that were + // previously encountered + component.slots.forEach((slot) => { + this.values.forEach((attribute, name) => { + if (!slot.values.has(name)) { + slot.values.set(name, attribute); + } + }); + }); + } else if (component.slots.has('default')) { + // otherwise, go the other way — inherit values from + // a previously encountered default slot + const default_slot = component.slots.get('default'); + default_slot.values.forEach((attribute, name) => { + if (!this.values.has(name)) { + this.values.set(name, attribute); + } + }); + } + + component.slots.set(this.slot_name, this); } } \ No newline at end of file diff --git a/src/compile/render-dom/Renderer.ts b/src/compile/render-dom/Renderer.ts index ae986315a9..1d766c6345 100644 --- a/src/compile/render-dom/Renderer.ts +++ b/src/compile/render-dom/Renderer.ts @@ -3,16 +3,16 @@ import { CompileOptions } from '../../interfaces'; import Component from '../Component'; import FragmentWrapper from './wrappers/Fragment'; import CodeBuilder from '../utils/CodeBuilder'; +import SlotWrapper from './wrappers/Slot'; export default class Renderer { component: Component; // TODO Maybe Renderer shouldn't know about Component? options: CompileOptions; - blocks: (Block | string)[]; - readonly: Set; - slots: Set; - meta_bindings: CodeBuilder; - binding_groups: string[]; + blocks: (Block | string)[] = []; + readonly: Set = new Set(); + meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a meta tag + binding_groups: string[] = []; block: Block; fragment: FragmentWrapper; @@ -24,16 +24,8 @@ export default class Renderer { this.options = options; this.locate = component.locate; // TODO messy - this.readonly = new Set(); - this.slots = new Set(); - this.file_var = options.dev && this.component.get_unique_name('file'); - // initial values for e.g. window.innerWidth, if there's a meta tag - this.meta_bindings = new CodeBuilder(); - - this.binding_groups = []; - // main block this.block = new Block({ renderer: this, @@ -46,7 +38,6 @@ export default class Renderer { }); this.block.has_update_method = true; - this.blocks = []; this.fragment = new FragmentWrapper( this, diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index 8ab5d25668..a6ef0ac69c 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -74,14 +74,14 @@ export default function dom( const props = component.vars.filter(variable => !variable.module && variable.export_name); const writable_props = props.filter(variable => variable.writable); - const set = (uses_props || writable_props.length > 0 || renderer.slots.size > 0) + const set = (uses_props || writable_props.length > 0 || component.slots.size > 0) ? deindent` ${$$props} => { ${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)} ${writable_props.map(prop => `if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};` )} - ${renderer.slots.size > 0 && + ${component.slots.size > 0 && `if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`} } ` @@ -285,7 +285,7 @@ export default function dom( } const args = ['$$self']; - if (props.length > 0 || component.has_reactive_assignments || renderer.slots.size > 0) { + if (props.length > 0 || component.has_reactive_assignments || component.slots.size > 0) { args.push('$$props', '$$invalidate'); } @@ -315,7 +315,7 @@ export default function dom( const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$'); - if (renderer.slots.size > 0) { + if (component.slots.size > 0) { filtered_declarations.push('$$slots', '$$scope'); } @@ -415,7 +415,7 @@ export default function dom( ${component.javascript} - ${renderer.slots.size && `let { $$slots = {}, $$scope } = $$props;`} + ${component.slots.size && `let { $$slots = {}, $$scope } = $$props;`} ${renderer.binding_groups.length > 0 && `const $$binding_groups = [${renderer.binding_groups.map(_ => `[]`).join(', ')}];`} diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 80a8308b93..22ea7a78cd 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -143,7 +143,14 @@ export default class ElementWrapper extends Wrapper { name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`) }); - const fn = get_context_merger(this.node.lets); + const lets = this.node.lets; + const seen = new Set(lets.map(l => l.name)); + + (owner as InlineComponentWrapper).node.lets.forEach(l => { + if (!seen.has(l.name)) lets.push(l); + }); + + const fn = get_context_merger(lets); (owner as InlineComponentWrapper).slots.set(name, { block: child_block, @@ -220,10 +227,6 @@ export default class ElementWrapper extends Wrapper { render(block: Block, parent_node: string, parent_nodes: string) { const { renderer } = this; - if (this.node.name === 'slot') { - renderer.slots.add((this.node as Slot).slot_name); - } - if (this.node.name === 'noscript') return; if (this.slot_block) { diff --git a/src/compile/render-dom/wrappers/Slot.ts b/src/compile/render-dom/wrappers/Slot.ts index 89a575910f..ec1b7d4b59 100644 --- a/src/compile/render-dom/wrappers/Slot.ts +++ b/src/compile/render-dom/wrappers/Slot.ts @@ -9,6 +9,7 @@ import add_to_set from '../../utils/add_to_set'; import get_slot_data from '../../utils/get_slot_data'; import { stringify_props } from '../../utils/stringify_props'; import Expression from '../../nodes/shared/Expression'; +import Attribute from '../../nodes/Attribute'; export default class SlotWrapper extends Wrapper { node: Slot; @@ -37,7 +38,7 @@ export default class SlotWrapper extends Wrapper { next_sibling ); - this.node.attributes.forEach(attribute => { + this.node.values.forEach(attribute => { add_to_set(this.dependencies, attribute.dependencies); }); @@ -56,23 +57,20 @@ export default class SlotWrapper extends Wrapper { const { renderer } = this; const { slot_name } = this.node; - renderer.slots.add(slot_name); let get_slot_changes; let get_slot_context; - const attributes = this.node.attributes.filter(attribute => attribute.name !== 'name'); + if (this.node.values.size > 0) { + get_slot_changes = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_changes`); + get_slot_context = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_context`); - if (attributes.length > 0) { - get_slot_changes = renderer.component.get_unique_name(`get_${slot_name}_slot_changes`); - get_slot_context = renderer.component.get_unique_name(`get_${slot_name}_slot_context`); - - const context_props = get_slot_data(attributes, false); + const context_props = get_slot_data(this.node.values, false); const changes_props = []; const dependencies = new Set(); - attributes.forEach(attribute => { + this.node.values.forEach(attribute => { attribute.chunks.forEach(chunk => { if ((chunk as Expression).dependencies) { add_to_set(dependencies, (chunk as Expression).dependencies); diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts index 0c2cdc489e..4c48c85133 100644 --- a/src/compile/render-ssr/handlers/Element.ts +++ b/src/compile/render-ssr/handlers/Element.ts @@ -51,13 +51,21 @@ export default function(node, renderer, options) { let textarea_contents; // awkward special case const slot = node.get_static_attribute_value('slot'); - if (slot && node.has_ancestor('InlineComponent')) { + const component = node.find_nearest(/InlineComponent/); + if (slot && component) { const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot'); const slot_name = slot.chunks[0].data; const target = renderer.targets[renderer.targets.length - 1]; target.slot_stack.push(slot_name); target.slots[slot_name] = ''; + const lets = node.lets; + const seen = new Set(lets.map(l => l.name)); + + component.lets.forEach(l => { + if (!seen.has(l.name)) lets.push(l); + }); + options.slot_scopes.set(slot_name, get_slot_scope(node.lets)); } diff --git a/src/compile/render-ssr/handlers/Slot.ts b/src/compile/render-ssr/handlers/Slot.ts index 77273e8009..b2e67f9e79 100644 --- a/src/compile/render-ssr/handlers/Slot.ts +++ b/src/compile/render-ssr/handlers/Slot.ts @@ -4,7 +4,7 @@ import get_slot_data from '../../utils/get_slot_data'; export default function(node, renderer, options) { const prop = quote_prop_if_necessary(node.slot_name); - const slot_data = get_slot_data(node.attributes, true); + const slot_data = get_slot_data(node.values, true); const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : ''; diff --git a/src/compile/utils/get_slot_data.ts b/src/compile/utils/get_slot_data.ts index e0a85aa348..ee64d8f1a0 100644 --- a/src/compile/utils/get_slot_data.ts +++ b/src/compile/utils/get_slot_data.ts @@ -1,8 +1,9 @@ import { snip } from './snip'; import { stringify_attribute } from './stringify_attribute'; +import Attribute from '../nodes/Attribute'; -export default function get_slot_data(attributes, is_ssr: boolean) { - return attributes +export default function get_slot_data(values: Map, is_ssr: boolean) { + return Array.from(values.values()) .filter(attribute => attribute.name !== 'name') .map(attribute => { const value = attribute.is_true diff --git a/test/runtime/samples/component-slot-named-inherits-default-lets/Nested.svelte b/test/runtime/samples/component-slot-named-inherits-default-lets/Nested.svelte new file mode 100644 index 0000000000..472af6278e --- /dev/null +++ b/test/runtime/samples/component-slot-named-inherits-default-lets/Nested.svelte @@ -0,0 +1,17 @@ + + +
+ + + + + +
\ No newline at end of file diff --git a/test/runtime/samples/component-slot-named-inherits-default-lets/_config.js b/test/runtime/samples/component-slot-named-inherits-default-lets/_config.js new file mode 100644 index 0000000000..212c57308a --- /dev/null +++ b/test/runtime/samples/component-slot-named-inherits-default-lets/_config.js @@ -0,0 +1,25 @@ +export default { + html: ` +
+

count in default slot: 0

+

count in foo slot: 0

+

count in bar slot: 0

+ +
+ `, + + async test({ assert, target, window }) { + const button = target.querySelector('button'); + + await button.dispatchEvent(new window.MouseEvent('click')); + + assert.htmlEqual(target.innerHTML, ` +
+

count in default slot: 1

+

count in foo slot: 1

+

count in bar slot: 1

+ +
+ `); + } +} \ No newline at end of file diff --git a/test/runtime/samples/component-slot-named-inherits-default-lets/main.svelte b/test/runtime/samples/component-slot-named-inherits-default-lets/main.svelte new file mode 100644 index 0000000000..51dd28b855 --- /dev/null +++ b/test/runtime/samples/component-slot-named-inherits-default-lets/main.svelte @@ -0,0 +1,17 @@ + + + +

+ count in default slot: {count} +

+ +

+ count in foo slot: {count} +

+ +

+ count in bar slot: {count} +

+
\ No newline at end of file