Skip to content

feat: conditional slots #8304

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

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7d93872
feat: conditional slots csr
tanhauhau Feb 21, 2023
69f8a91
implement conditional slot for ssr
tanhauhau Feb 22, 2023
004b559
feat: support const tags in conditional slots
tanhauhau Feb 23, 2023
571dd6e
feat: rename test
tanhauhau Feb 23, 2023
8edc682
feat: dynamic $$slots
tanhauhau Feb 23, 2023
f9108d0
fix lint
tanhauhau Feb 23, 2023
07d4350
support <elem slot=...> in slots forwarding
tanhauhau Feb 23, 2023
31ba89e
fix test
tanhauhau Feb 28, 2023
9acdaea
feat: conditional slots csr
tanhauhau Feb 21, 2023
54f6569
implement conditional slot for ssr
tanhauhau Feb 22, 2023
58ddbde
feat: support const tags in conditional slots
tanhauhau Feb 23, 2023
a59698e
feat: rename test
tanhauhau Feb 23, 2023
4365862
feat: dynamic $$slots
tanhauhau Feb 23, 2023
76f4bc5
fix lint
tanhauhau Feb 23, 2023
4716da4
support <elem slot=...> in slots forwarding
tanhauhau Feb 23, 2023
dde5f30
fix test
tanhauhau Feb 28, 2023
765023d
fix: html space entities lost in component slot (#8464)
xxkl1 Apr 11, 2023
75aec41
breaking: send in/out to transition fn (#8318)
tivac Apr 11, 2023
c729829
chore: remove node<14 tests (#8482)
dummdidumm Apr 11, 2023
eedacc9
chore: simplify Svelte 4 CI (#8487)
benmccann Apr 12, 2023
abd760d
chore: bump engines field (#8489)
benmccann Apr 12, 2023
1e2cfa4
chore: upgrade to TypeScript 5 (#8488)
benmccann Apr 12, 2023
42e0f7d
chore: Svelte 4 dependency upgrades (#8486)
benmccann Apr 12, 2023
c9ccd6e
chore: upgrade rollup (#8491)
benmccann Apr 12, 2023
573784c
chore: run fewer CI jobs (#8496)
benmccann Apr 13, 2023
d6bcddd
breaking: improve types for `createEventDispatcher` (#7224)
ivanhofer Apr 14, 2023
56a6738
breaking: conditional ActionReturn type if Parameter is void (#7442)
tanhauhau Apr 14, 2023
9460616
feat: add `a11y-no-static-element-interactions` compiler rule (#8251)
timmcca-be Apr 14, 2023
88728e3
fix: bind null option and input values consistently (#8328)
theodorejb Apr 14, 2023
d1a9722
feat: add a11y `no-noninteractive-element-interactions` (#8391)
ngtr6788 Apr 14, 2023
b6032ee
Merge branch 'version-4' into feat/conditional-slots
dummdidumm Apr 14, 2023
c936771
Merge branch 'feat/conditional-slots' of https://github.com/tanhauhau…
dummdidumm Apr 18, 2023
4a652b5
Merge branch 'version-4' into feat/conditional-slots
dummdidumm Apr 18, 2023
f16762f
remove log
dummdidumm Apr 18, 2023
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
6 changes: 4 additions & 2 deletions src/compiler/compile/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export default class Component {
this.add_var(node, {
name,
injected: true,
referenced: true
referenced: true,
reassigned: name === '$$slots'
});
} else if (name[0] === '$') {
this.add_var(node, {
Expand Down Expand Up @@ -720,7 +721,8 @@ export default class Component {
} else if (is_reserved_keyword(name)) {
this.add_var(node, {
name,
injected: true
injected: true,
reassigned: name === '$$slots'
});
} else if (name[0] === '$') {
if (name === '$' || name[1] === '$') {
Expand Down
11 changes: 11 additions & 0 deletions src/compiler/compile/compiler_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <svelte:fragment> and other elements under the same {#if}{:else} group. Default slot content should be wrapped with <svelte:fragment slot="default">'
},
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}>`
})
};
2 changes: 1 addition & 1 deletion src/compiler/compile/nodes/ConstTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import get_object from '../utils/get_object';
import compiler_errors from '../compiler_errors';
import { Node as ESTreeNode } from 'estree';

const allowed_parents = new Set(['EachBlock', 'CatchBlock', 'ThenBlock', 'InlineComponent', 'SlotTemplate', 'IfBlock', 'ElseBlock']);
const allowed_parents = new Set(['EachBlock', 'CatchBlock', 'ThenBlock', 'InlineComponent', 'SlotTemplate', 'IfBlock', 'ElseBlock', 'SlotTemplateIfBlock', 'SlotTemplateElseBlock']);

export default class ConstTag extends Node {
type: 'ConstTag';
Expand Down
66 changes: 9 additions & 57 deletions src/compiler/compile/nodes/InlineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import { regex_only_whitespaces } from '../../utils/patterns';
import { validate_get_slot_names } from './SlotTemplateIfBlock';
import { extract_children_to_slot_templates } from './extract_children_to_slot_templates';

export default class InlineComponent extends Node {
type: 'InlineComponent';
Expand Down Expand Up @@ -52,7 +53,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;
Expand Down Expand Up @@ -106,69 +107,20 @@ export default class InlineComponent extends Node {
});
});

const children = [];
for (let i = info.children.length - 1; i >= 0; i--) {
const child = info.children[i];
if (child.type === 'SlotTemplate') {
children.push(child);
info.children.splice(i, 1);
} else if ((child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') && child.attributes.find(attribute => attribute.name === 'slot')) {
const slot_template = {
start: child.start,
end: child.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: [child]
};

// transfer attributes
for (let i = child.attributes.length - 1; i >= 0; i--) {
const attribute = child.attributes[i];
if (attribute.type === 'Let') {
slot_template.attributes.push(attribute);
child.attributes.splice(i, 1);
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
slot_template.attributes.push(attribute);
}
}
// transfer const
for (let i = child.children.length - 1; i >= 0; i--) {
const child_child = child.children[i];
if (child_child.type === 'ConstTag') {
slot_template.children.push(child_child);
child.children.splice(i, 1);
}
}

children.push(slot_template);
info.children.splice(i, 1);
} else if (child.type === 'Comment' && children.length > 0) {
children[children.length - 1].children.unshift(child);
}
}

if (info.children.some(node => not_whitespace_text(node))) {
children.push({
start: info.start,
end: info.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: info.children
});
}
const children = extract_children_to_slot_templates(component, info, true);

this.children = map_children(component, this, this.scope, children);

this.validate_duplicate_slot_name();
}

get slot_template_name() {
return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
}
}

function not_whitespace_text(node) {
return !(node.type === 'Text' && regex_only_whitespaces.test(node.data));
validate_duplicate_slot_name() {
validate_get_slot_names(this.children, this.component, this.name);
}
}

function get_namespace(parent: Node, explicit_namespace: string) {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/compile/nodes/Slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';

export default class Slot extends Element {
type: 'Element';
// @ts-ignore unable to override the type from Element, but this give us a right type
type: 'Slot';
name: string;
children: INode[];
slot_name: string;
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/compile/nodes/SlotTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
47 changes: 47 additions & 0 deletions src/compiler/compile/nodes/SlotTemplateElseBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Component from '../Component';
import Expression from './shared/Expression';
import TemplateScope from './shared/TemplateScope';
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));
}
}
73 changes: 73 additions & 0 deletions src/compiler/compile/nodes/SlotTemplateIfBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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 SlotTemplate from './SlotTemplate';
import { INode } from './interfaces';
import { extract_children_to_slot_templates } from './extract_children_to_slot_templates';

export default class SlotTemplateIfBlock extends AbstractBlock {
type: 'SlotTemplateIfBlock';
expression: Expression;
else: SlotTemplateElseBlock;
scope: TemplateScope;
const_tags: ConstTag[];
slot_names = new Set<string>();

constructor(
component: Component,
parent: Node,
scope: TemplateScope,
info: TemplateNode
) {
super(component, parent, scope, info);
this.scope = scope.child();

const children = extract_children_to_slot_templates(component, info, false);

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<string, SlotTemplate> {
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: INode[], component: Component, component_name: string) {
const slot_names = new Map<string, SlotTemplate>();
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;
}
107 changes: 107 additions & 0 deletions src/compiler/compile/nodes/extract_children_to_slot_templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { x } from 'code-red';
import { TemplateNode } from '../../interfaces';
import { regex_only_whitespaces } from '../../utils/patterns';
import compiler_errors from '../compiler_errors';
import Component from '../Component';

export function extract_children_to_slot_templates(component: Component, node: TemplateNode, extract_default_slot: boolean) {
const result = [];
for (let i = node.children.length - 1; i >= 0; i--) {
const child = node.children[i];
if (child.type === 'SlotTemplate') {
result.push(child);
node.children.splice(i, 1);
} else if ((child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') && child.attributes.find(attribute => attribute.name === 'slot')) {
let slot_template = {
start: child.start,
end: child.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: [child]
};

// transfer attributes
for (let i = child.attributes.length - 1; i >= 0; i--) {
const attribute = child.attributes[i];
if (attribute.type === 'Let') {
slot_template.attributes.push(attribute);
child.attributes.splice(i, 1);
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
slot_template.attributes.push(attribute);
}
}
// transfer const
for (let i = child.children.length - 1; i >= 0; i--) {
const child_child = child.children[i];
if (child_child.type === 'ConstTag') {
slot_template.children.push(child_child);
child.children.splice(i, 1);
}
}

// if the <slot slot="x"> does not have any fallback
// then we make <slot slot="x" name="b" />
// into {#if $$slots.x}<slot slot="x" name="b" />{/if}
// this makes the slots forwarding to passthrough
if (child.type === 'Slot' && child.children.length === 0) {
const slot_template_name = child.attributes.find(attribute => attribute.name === 'name')?.value[0].data ?? 'default';
slot_template = {
start: slot_template.start,
end: slot_template.end,
type: 'SlotTemplateIfBlock',
expression: x`$$slots.${slot_template_name}`,
children: [slot_template]
} as any;
}

result.push(slot_template);
node.children.splice(i, 1);
} else if (child.type === 'Comment' && result.length > 0) {
result[result.length - 1].children.unshift(child);
node.children.splice(i, 1);
} else if (child.type === 'Text' && regex_only_whitespaces.test(child.data)) {
// ignore
} else if (child.type === 'ConstTag') {
if (!extract_default_slot) {
result.push(child);
}
} else if (child.type === 'IfBlock' && if_block_contains_slot_template(child)) {
result.push({
...child,
type: 'SlotTemplateIfBlock'
});
node.children.splice(i, 1);
} else if (!extract_default_slot) {
component.error(child, compiler_errors.invalid_mix_element_and_conditional_slot);
}
}

if (extract_default_slot) {
if (node.children.some(node => not_whitespace_text(node))) {
result.push({
start: node.start,
end: node.end,
type: 'SlotTemplate',
name: 'svelte:fragment',
attributes: [],
children: node.children
});
}
}
return result.reverse();
}


function not_whitespace_text(node) {
return !(node.type === 'Text' && regex_only_whitespaces.test(node.data));
}

function if_block_contains_slot_template(node: TemplateNode) {
for (const child of node.children) {
if (child.type === 'SlotTemplate') return true;
if (child.type === 'IfBlock' && if_block_contains_slot_template(child)) return true;
if ((child.type === 'Element' || child.type === 'InlineComponent' || child.type === 'Slot') && child.attributes.find(attribute => attribute.name === 'slot')) return true;
}
return false;
}
Loading