Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dynamic slots #8535

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/compiler/compile/compiler_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ export default {
code: 'illegal-attribute',
message: `'${name}' is not a valid attribute name`
}),
invalid_slot_attribute: {
code: 'invalid-slot-attribute',
message: 'slot attribute cannot have a dynamic value'
},
duplicate_slot_attribute: (name: string) => ({
code: 'duplicate-slot-attribute',
message: `Duplicate '${name}' slot`
Expand Down Expand Up @@ -132,10 +128,6 @@ export default {
code: 'invalid-slot-directive',
message: '<slot> cannot have directives'
},
dynamic_slot_name: {
code: 'dynamic-slot-name',
message: '<slot> name cannot be dynamic'
},
invalid_slot_name: {
code: 'invalid-slot-name',
message: 'default is a reserved word — it cannot be used as a slot name'
Expand Down
4 changes: 0 additions & 4 deletions src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,6 @@ export default class Element extends Node {
}

if (name === 'slot') {
if (!attribute.is_static) {
return component.error(attribute, compiler_errors.invalid_slot_attribute);
}

if (component.slot_outlets.has(name)) {
return component.error(attribute, compiler_errors.duplicate_slot_attribute(name));

Expand Down
29 changes: 21 additions & 8 deletions src/compiler/compile/nodes/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,52 @@ import Attribute from './Attribute';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import { TemplateNode, Attribute as AttributeNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';

export default class Slot extends Element {
type: 'Element';
name: string;
children: INode[];
slot_name: string;
name_attribute: Attribute;
values: Map<string, Attribute> = new Map();

constructor(component: Component, parent: INode, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);

info.attributes.forEach(attr => {
info.attributes.forEach((attr: AttributeNode) => {
if (attr.type !== 'Attribute' && attr.type !== 'Spread') {
return component.error(attr, compiler_errors.invalid_slot_directive);
}


const new_attribute = new Attribute(component, this, scope, attr);
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
return component.error(attr, compiler_errors.dynamic_slot_name);
if (attr.value.length === 1 && attr.value[0].type === 'Text') {
this.slot_name = attr.value[0].data;
} else {
this.slot_name = component.get_unique_name('dynamic_slot_name').name;
}

this.slot_name = attr.value[0].data;
if (this.slot_name === 'default') {
return component.error(attr, compiler_errors.invalid_slot_name);
}

this.name_attribute = new_attribute;
}

this.values.set(attr.name, new Attribute(component, this, scope, attr));
this.values.set(attr.name, new_attribute);
});

if (!this.slot_name) this.slot_name = 'default';
if (!this.slot_name) {
// If there is no name attribute, pretend we do have a name attribute with value 'default'
this.slot_name = 'default';
this.name_attribute = new Attribute(component, this, scope, {
type: 'Attribute',
name: 'name',
value: [ { type: 'Text', data: 'default' } ]
} as AttributeNode);
}

if (this.slot_name === 'default') {
// if this is the default slot, add our dependencies to any
Expand Down
31 changes: 23 additions & 8 deletions src/compiler/compile/nodes/SlotTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { INode } from './interfaces';
import compiler_errors from '../compiler_errors';
import get_const_tags from './shared/get_const_tags';
import ConstTag from './ConstTag';
import { Attribute as AttributeNode } from '../../interfaces';

export default class SlotTemplate extends Node {
type: 'SlotTemplate';
Expand All @@ -15,7 +16,8 @@ export default class SlotTemplate extends Node {
lets: Let[] = [];
const_tags: ConstTag[];
slot_attribute: Attribute;
slot_template_name: string = 'default';
is_static: boolean;
slot_template_name: string;

constructor(
component: Component,
Expand Down Expand Up @@ -44,14 +46,17 @@ export default class SlotTemplate extends Node {
case 'Attribute': {
if (node.name === 'slot') {
this.slot_attribute = new Attribute(component, this, scope, node);
if (!this.slot_attribute.is_static) {
return component.error(node, compiler_errors.invalid_slot_attribute);
if (this.slot_attribute.is_static) {
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
this.is_static = true;
} else {
this.slot_template_name = component.get_unique_name('dynamic_slot_template').name;
this.is_static = false;
}
const value = this.slot_attribute.get_static_value();
if (typeof value === 'boolean') {
return component.error(node, compiler_errors.invalid_slot_attribute_value_missing);
}
this.slot_template_name = value as string;
break;
}
throw new Error(`Invalid attribute '${node.name}' in <svelte:fragment>`);
Expand All @@ -61,6 +66,16 @@ export default class SlotTemplate extends Node {
}
});

if (!this.slot_template_name) {
this.slot_template_name = 'default';
this.is_static = true;
this.slot_attribute = new Attribute(component, this, scope, {
type: 'Attribute',
name: 'slot',
value: [ { type: 'Text', data: 'default' } ]
} as AttributeNode);
}

this.scope = scope;
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
}
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/compile/render_dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { flatten } from '../../utils/flatten';
import check_enable_sourcemap from '../utils/check_enable_sourcemap';
import { push_array } from '../../utils/push_array';
import { regex_backslashes } from '../../utils/patterns';
import { get_attribute_value } from '../render_ssr/handlers/shared/get_attribute_value';

export default function dom(
component: Component,
Expand Down Expand Up @@ -459,6 +460,11 @@ export default function dom(
}) as Expression)
};

const slot_list = {
type: 'ArrayExpression',
elements: [...component.slots.values()].map(slot => get_attribute_value(slot.name_attribute))
};

body.push(b`
function ${definition}(${args}) {
${injected.map(name => b`let ${name};`)}
Expand All @@ -472,11 +478,11 @@ export default function dom(
${resubscribable_reactive_store_unsubscribers}

${component.slots.size || component.compile_options.dev || uses_slots ? b`let { $$slots: #slots = {}, $$scope } = $$props;` : null}
${component.compile_options.dev && b`@validate_slots('${component.tag}', #slots, [${[...component.slots.keys()].map(key => `'${key}'`).join(',')}]);`}
${compute_slots}

${instance_javascript}

${component.compile_options.dev && b`@validate_slots('${component.tag}', #slots, ${slot_list});`}
${missing_props_check}
${unknown_props_check}

Expand Down
27 changes: 18 additions & 9 deletions src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g;

export default class InlineComponentWrapper extends Wrapper {
var: Identifier;
slots: Map<string, SlotDefinition> = new Map();
slots: Map<SlotTemplate, SlotDefinition> = new Map();
static_slot_names: Set<string> = new Set();
node: InlineComponent;
fragment: FragmentWrapper;
children: Array<Wrapper | FragmentWrapper> = [];
Expand Down Expand Up @@ -95,14 +96,20 @@ export default class InlineComponentWrapper extends Wrapper {
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"');
set_slot(slot: SlotTemplate, slot_definition: SlotDefinition) {
if (slot.is_static) {
const name = slot.slot_template_name;
if (this.static_slot_names.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}>`);
} else {
this.static_slot_names.add(name);
}
throw new Error(`Duplicate slot name "${name}" in <${this.node.name}>`);
}
this.slots.set(name, slot_definition);

this.slots.set(slot, slot_definition);
}

warn_if_reactive() {
Expand Down Expand Up @@ -167,8 +174,10 @@ export default class InlineComponentWrapper extends Wrapper {
const initial_props = this.slots.size > 0
? [
p`$$slots: {
${Array.from(this.slots).map(([name, slot]) => {
return p`${name}: [${slot.block.name}, ${slot.get_context || null}, ${slot.get_changes || null}]`;
${Array.from(this.slots).map(([slot_template, slot]) => {
const { slot_attribute } = slot_template;
const slot_expression = slot_attribute.get_value(block);
return p`[${slot_expression}]: [${slot.block.name}, ${slot.get_context || null}, ${slot.get_changes || null}]`;
})}
}`,
p`$$scope: {
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/compile/render_dom/wrappers/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default class SlotWrapper extends Wrapper {
) {
const { renderer } = this;

const { slot_name } = this.node;
const { slot_name, name_attribute } = this.node;

if (this.slot_block) {
block = this.slot_block;
Expand Down Expand Up @@ -128,8 +128,9 @@ export default class SlotWrapper extends Wrapper {
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 slot_expression = name_attribute.get_value(block);
block.chunks.init.push(b`
const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name};
const ${slot_definition} = ${renderer.reference('#slots')}[${slot_expression}];
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}
`);
Expand Down
5 changes: 1 addition & 4 deletions src/compiler/compile/render_dom/wrappers/SlotTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ export default class SlotTemplateWrapper extends Wrapper {
if (!seen.has(l.name.name)) lets.push(l);
});

this.parent.set_slot(
slot_template_name,
get_slot_definition(this.block, scope, lets)
);
this.parent.set_slot(this.node, get_slot_definition(this.block, scope, lets));

this.fragment = new FragmentWrapper(
renderer,
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/compile/render_ssr/handlers/InlineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
slot_scopes
}));

slot_scopes.forEach(({ input, output, statements }, name) => {
slot_scopes.forEach(({ input, output, statements }, slot_exp) => {
slot_fns.push(
p`${name}: (${input}) => { ${statements}; return ${output}; }`
p`[${slot_exp}]: (${input}) => { ${statements}; return ${output}; }`
);
});
}
Expand Down
8 changes: 5 additions & 3 deletions src/compiler/compile/render_ssr/handlers/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import Renderer, { RenderOptions } from '../Renderer';
import Slot from '../../nodes/Slot';
import { x } from 'code-red';
import get_slot_data from '../../utils/get_slot_data';
import { get_attribute_value } from './shared/get_attribute_value';
import { get_slot_scope } from './shared/get_slot_scope';

export default function(node: Slot, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
const slot_data = get_slot_data(node.values);
const slot = node.get_static_attribute_value('slot');
const slot = node.values.get('slot')?.get_value(null);
const nearest_inline_component = node.find_nearest(/InlineComponent/);

if (slot && nearest_inline_component) {
Expand All @@ -19,9 +20,10 @@ export default function(node: Slot, renderer: Renderer, options: RenderOptions &
renderer.render(node.children, options);
const result = renderer.pop();

const slot_expression = get_attribute_value(node.name_attribute);
renderer.add_expression(x`
#slots.${node.slot_name}
? #slots.${node.slot_name}(${slot_data})
#slots[${slot_expression}]
? #slots[${slot_expression}](${slot_data})
: ${result}
`);

Expand Down
4 changes: 3 additions & 1 deletion src/compiler/compile/render_ssr/handlers/SlotTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import remove_whitespace_children from './utils/remove_whitespace_children';
import { get_slot_scope } from './shared/get_slot_scope';
import InlineComponent from '../../nodes/InlineComponent';
import { get_const_tags } from './shared/get_const_tags';
import { get_attribute_value } from './shared/get_attribute_value';

export default function(node: SlotTemplate, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
Expand All @@ -29,7 +30,8 @@ export default function(node: SlotTemplate, renderer: Renderer, options: RenderO
throw new Error(`Duplicate slot name "${node.slot_template_name}" in <${parent_inline_component.name}>`);
}

options.slot_scopes.set(node.slot_template_name, {
const slot_expression = get_attribute_value(node.slot_attribute);
options.slot_scopes.set(slot_expression, {
input: get_slot_scope(node.lets),
output: slot_fragment_content,
statements: get_const_tags(node.const_tags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function get_attribute_value(attribute: Attribute): ESTreeExpression {
* For value attribute of textarea, it will render as child node of `<textarea>` element.
* Therefore, we need to escape as content (not attribute).
*/
const is_textarea_value = attribute.parent.name.toLowerCase() === 'textarea' && attribute.name.toLowerCase() === 'value';
const is_textarea_value = attribute.parent.name?.toLowerCase() === 'textarea' && attribute.name?.toLowerCase() === 'value';

return attribute.chunks
.map((chunk) => {
Expand Down
4 changes: 2 additions & 2 deletions test/js/samples/capture-inject-state/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ function instance($$self, $$props, $$invalidate) {

$$self.$$.on_destroy.push(() => $$unsubscribe_prop());
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots('Component', slots, []);
let { prop } = $$props;
validate_store(prop, 'prop');
$$subscribe_prop();
let { alias: realName } = $$props;
let local;
let shadowedByModule;
validate_slots('Component', slots, []);

$$self.$$.on_mount.push(function () {
if (prop === undefined && !('prop' in $$props || $$self.$$.bound[$$self.$$.props['prop']])) {
Expand Down Expand Up @@ -197,4 +197,4 @@ class Component extends SvelteComponentDev {
}

export default Component;
export { moduleLiveBinding, moduleConstantProps };
export { moduleLiveBinding, moduleConstantProps };
2 changes: 1 addition & 1 deletion test/js/samples/debug-empty/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ function create_fragment(ctx) {

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots('Component', slots, []);
let { name } = $$props;
validate_slots('Component', slots, []);

$$self.$$.on_mount.push(function () {
if (name === undefined && !('name' in $$props || $$self.$$.bound[$$self.$$.props['name']])) {
Expand Down
2 changes: 1 addition & 1 deletion test/js/samples/debug-foo-bar-baz-things/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ function create_fragment(ctx) {

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots('Component', slots, []);
let { things } = $$props;
let { foo } = $$props;
let { bar } = $$props;
let { baz } = $$props;
validate_slots('Component', slots, []);

$$self.$$.on_mount.push(function () {
if (things === undefined && !('things' in $$props || $$self.$$.bound[$$self.$$.props['things']])) {
Expand Down
2 changes: 1 addition & 1 deletion test/js/samples/debug-foo/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ function create_fragment(ctx) {

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots('Component', slots, []);
let { things } = $$props;
let { foo } = $$props;
validate_slots('Component', slots, []);

$$self.$$.on_mount.push(function () {
if (things === undefined && !('things' in $$props || $$self.$$.bound[$$self.$$.props['things']])) {
Expand Down
Loading