Merge pull request #2419 from sveltejs/gh-2320

Expose default slot values to named slots
pull/2420/head
Rich Harris 6 years ago committed by GitHub
commit 158ce2eea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -20,6 +20,7 @@ import fuzzymatch from '../utils/fuzzymatch';
import { remove_indentation, add_indentation } from '../utils/indentation'; import { remove_indentation, add_indentation } from '../utils/indentation';
import get_object from './utils/get_object'; import get_object from './utils/get_object';
import unwrap_parens from './utils/unwrap_parens'; import unwrap_parens from './utils/unwrap_parens';
import Slot from './nodes/Slot';
type ComponentOptions = { type ComponentOptions = {
namespace?: string; namespace?: string;
@ -117,6 +118,9 @@ export default class Component {
used_names: Set<string> = new Set(); used_names: Set<string> = new Set();
globally_used_names: Set<string> = new Set(); globally_used_names: Set<string> = new Set();
slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set();
constructor( constructor(
ast: Ast, ast: Ast,
source: string, source: string,

@ -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; let ancestor = this.parent;
do { do {
if (ancestor.type === 'InlineComponent') break; if (ancestor.type === 'InlineComponent') break;

@ -1,15 +1,17 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Element from './Element'; import Element from './Element';
import Attribute from './Attribute'; import Attribute from './Attribute';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
export default class Slot extends Element { export default class Slot extends Element {
type: 'Element'; type: 'Element';
name: string; name: string;
slot_name: string;
attributes: Attribute[];
children: Node[]; children: Node[];
slot_name: string;
values: Map<string, Attribute> = new Map();
constructor(component, parent, scope, info) { constructor(component: Component, parent: Node, scope: TemplateScope, info: any) {
super(component, parent, scope, info); super(component, parent, scope, info);
info.attributes.forEach(attr => { 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 this.values.set(attr.name, new Attribute(component, this, scope, attr));
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slot_name)) {
// validator.error(`duplicate '${slot_name}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slot_name);
}); });
if (!this.slot_name) this.slot_name = 'default'; if (!this.slot_name) this.slot_name = 'default';
// if (node.attributes.length === 0) && validator.slots.has('default')) { if (this.slot_name === 'default') {
// validator.error(node, { // if this is the default slot, add our dependencies to any
// code: `duplicate-slot`, // other slots (which inherit our slot values) that were
// message: `duplicate default <slot> element` // 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);
} }
} }

@ -3,16 +3,16 @@ import { CompileOptions } from '../../interfaces';
import Component from '../Component'; import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment'; import FragmentWrapper from './wrappers/Fragment';
import CodeBuilder from '../utils/CodeBuilder'; import CodeBuilder from '../utils/CodeBuilder';
import SlotWrapper from './wrappers/Slot';
export default class Renderer { export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component? component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions; options: CompileOptions;
blocks: (Block | string)[]; blocks: (Block | string)[] = [];
readonly: Set<string>; readonly: Set<string> = new Set();
slots: Set<string>; meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
meta_bindings: CodeBuilder; binding_groups: string[] = [];
binding_groups: string[];
block: Block; block: Block;
fragment: FragmentWrapper; fragment: FragmentWrapper;
@ -24,16 +24,8 @@ export default class Renderer {
this.options = options; this.options = options;
this.locate = component.locate; // TODO messy 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'); this.file_var = options.dev && this.component.get_unique_name('file');
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.meta_bindings = new CodeBuilder();
this.binding_groups = [];
// main block // main block
this.block = new Block({ this.block = new Block({
renderer: this, renderer: this,
@ -46,7 +38,6 @@ export default class Renderer {
}); });
this.block.has_update_method = true; this.block.has_update_method = true;
this.blocks = [];
this.fragment = new FragmentWrapper( this.fragment = new FragmentWrapper(
this, this,

@ -74,14 +74,14 @@ export default function dom(
const props = component.vars.filter(variable => !variable.module && variable.export_name); const props = component.vars.filter(variable => !variable.module && variable.export_name);
const writable_props = props.filter(variable => variable.writable); 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` ? deindent`
${$$props} => { ${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)} ${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_props.map(prop => ${writable_props.map(prop =>
`if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};` `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`)};`} `if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`}
} }
` `
@ -285,7 +285,7 @@ export default function dom(
} }
const args = ['$$self']; 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'); 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] !== '$'); 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'); filtered_declarations.push('$$slots', '$$scope');
} }
@ -415,7 +415,7 @@ export default function dom(
${component.javascript} ${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(', ')}];`} ${renderer.binding_groups.length > 0 && `const $$binding_groups = [${renderer.binding_groups.map(_ => `[]`).join(', ')}];`}

@ -143,7 +143,14 @@ export default class ElementWrapper extends Wrapper {
name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`) 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, { (owner as InlineComponentWrapper).slots.set(name, {
block: child_block, block: child_block,
@ -220,10 +227,6 @@ export default class ElementWrapper extends Wrapper {
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, parent_node: string, parent_nodes: string) {
const { renderer } = this; 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.node.name === 'noscript') return;
if (this.slot_block) { if (this.slot_block) {

@ -9,6 +9,7 @@ import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data'; import get_slot_data from '../../utils/get_slot_data';
import { stringify_props } from '../../utils/stringify_props'; import { stringify_props } from '../../utils/stringify_props';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
import Attribute from '../../nodes/Attribute';
export default class SlotWrapper extends Wrapper { export default class SlotWrapper extends Wrapper {
node: Slot; node: Slot;
@ -37,7 +38,7 @@ export default class SlotWrapper extends Wrapper {
next_sibling next_sibling
); );
this.node.attributes.forEach(attribute => { this.node.values.forEach(attribute => {
add_to_set(this.dependencies, attribute.dependencies); add_to_set(this.dependencies, attribute.dependencies);
}); });
@ -56,23 +57,20 @@ export default class SlotWrapper extends Wrapper {
const { renderer } = this; const { renderer } = this;
const { slot_name } = this.node; const { slot_name } = this.node;
renderer.slots.add(slot_name);
let get_slot_changes; let get_slot_changes;
let get_slot_context; 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) { const context_props = get_slot_data(this.node.values, false);
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 changes_props = []; const changes_props = [];
const dependencies = new Set(); const dependencies = new Set();
attributes.forEach(attribute => { this.node.values.forEach(attribute => {
attribute.chunks.forEach(chunk => { attribute.chunks.forEach(chunk => {
if ((chunk as Expression).dependencies) { if ((chunk as Expression).dependencies) {
add_to_set(dependencies, (chunk as Expression).dependencies); add_to_set(dependencies, (chunk as Expression).dependencies);

@ -51,13 +51,21 @@ export default function(node, renderer, options) {
let textarea_contents; // awkward special case let textarea_contents; // awkward special case
const slot = node.get_static_attribute_value('slot'); 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 = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const slot_name = slot.chunks[0].data; const slot_name = slot.chunks[0].data;
const target = renderer.targets[renderer.targets.length - 1]; const target = renderer.targets[renderer.targets.length - 1];
target.slot_stack.push(slot_name); target.slot_stack.push(slot_name);
target.slots[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)); options.slot_scopes.set(slot_name, get_slot_scope(node.lets));
} }

@ -4,7 +4,7 @@ import get_slot_data from '../../utils/get_slot_data';
export default function(node, renderer, options) { export default function(node, renderer, options) {
const prop = quote_prop_if_necessary(node.slot_name); 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(', ')} }` : ''; const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : '';

@ -1,8 +1,9 @@
import { snip } from './snip'; import { snip } from './snip';
import { stringify_attribute } from './stringify_attribute'; import { stringify_attribute } from './stringify_attribute';
import Attribute from '../nodes/Attribute';
export default function get_slot_data(attributes, is_ssr: boolean) { export default function get_slot_data(values: Map<string, Attribute>, is_ssr: boolean) {
return attributes return Array.from(values.values())
.filter(attribute => attribute.name !== 'name') .filter(attribute => attribute.name !== 'name')
.map(attribute => { .map(attribute => {
const value = attribute.is_true const value = attribute.is_true

@ -0,0 +1,17 @@
<script>
import { onDestroy } from 'svelte';
let count = 0;
function increment() {
count += 1;
}
</script>
<div>
<slot {count}></slot>
<slot name="foo" {count}></slot>
<slot name="bar"></slot>
<button on:click={increment}>+1</button>
</div>

@ -0,0 +1,25 @@
export default {
html: `
<div>
<p>count in default slot: 0</p>
<p slot="foo">count in foo slot: 0</p>
<p slot="bar">count in bar slot: 0</p>
<button>+1</button>
</div>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
await button.dispatchEvent(new window.MouseEvent('click'));
assert.htmlEqual(target.innerHTML, `
<div>
<p>count in default slot: 1</p>
<p slot="foo">count in foo slot: 1</p>
<p slot="bar">count in bar slot: 1</p>
<button>+1</button>
</div>
`);
}
}

@ -0,0 +1,17 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested let:count>
<p>
count in default slot: {count}
</p>
<p slot="foo" let:count>
count in foo slot: {count}
</p>
<p slot="bar">
count in bar slot: {count}
</p>
</Nested>
Loading…
Cancel
Save