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 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<string> = new Set();
globally_used_names: Set<string> = new Set();
slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set();
constructor(
ast: Ast,
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;
do {
if (ancestor.type === 'InlineComponent') break;

@ -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<string, Attribute> = 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}' <slot> 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 <slot> 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);
}
}

@ -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<string>;
slots: Set<string>;
meta_bindings: CodeBuilder;
binding_groups: string[];
blocks: (Block | string)[] = [];
readonly: Set<string> = new Set();
meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a <svelte:window> 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 <svelte:window> 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,

@ -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(', ')}];`}

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

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

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

@ -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(', ')} }` : '';

@ -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<string, Attribute>, is_ssr: boolean) {
return Array.from(values.values())
.filter(attribute => attribute.name !== 'name')
.map(attribute => {
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