Skip to content

Programmic slot access/api #4604

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 4 commits into from
Closed
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
7 changes: 7 additions & 0 deletions src/compiler/compile/render_dom/Block.ts
Original file line number Diff line number Diff line change
@@ -75,6 +75,8 @@ export default class Block {
variables: Map<string, { id: Identifier; init?: Node }> = new Map();
get_unique_name: (name: string) => Identifier;

root_nodes: Identifier[] = [];

has_update_method = false;
autofocus: string;

@@ -269,9 +271,14 @@ export default class Block {
: this.chunks.hydrate
);

const return_value = this.type === 'slot'
? b`return [${this.root_nodes}]`
: null;

properties.create = x`function #create() {
${this.chunks.create}
${hydrate}
${return_value}
}`;
}

2 changes: 1 addition & 1 deletion src/compiler/compile/render_dom/Renderer.ts
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ export default class Renderer {

if (component.slots.size > 0) {
this.add_to_context('$$scope');
this.add_to_context('$$slots');
this.add_to_context('#slots');
}

if (this.binding_groups.length > 0) {
29 changes: 24 additions & 5 deletions src/compiler/compile/render_dom/index.ts
Original file line number Diff line number Diff line change
@@ -70,6 +70,20 @@ export default function dom(
);
}

const uses_slots = component.var_lookup.has('$$slots');
let slots = null;
let slots_update = null;

if (uses_slots) {
slots = b`let { $$slots, update: #update_$$slots } = @create_slots_accessor(#slots, $$scope)`;
slots_update = b`
if (${renderer.dirty(['$$scope'], true)}) {
#update_$$slots($$scope)
}
`;
renderer.add_to_context('$$scope');
}

const uses_props = component.var_lookup.has('$$props');
const uses_rest = component.var_lookup.has('$$restProps');
const $$props = uses_props || uses_rest ? `$$new_props` : `$$props`;
@@ -83,7 +97,7 @@ export default function dom(
let $$restProps = ${compute_rest};
` : null;

const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0)
const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0 || uses_slots)
? x`
${$$props} => {
${uses_props && renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)}
@@ -92,7 +106,7 @@ export default function dom(
${writable_props.map(prop =>
b`if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.export_name}`)};`
)}
${component.slots.size > 0 &&
${(component.slots.size > 0 || uses_slots) &&
b`if ('$$scope' in ${$$props}) ${renderer.invalidate('$$scope', x`$$scope = ${$$props}.$$scope`)};`}
}
`
@@ -420,12 +434,15 @@ export default function dom(

${resubscribable_reactive_store_unsubscribers}

${component.slots.size || uses_slots || component.compile_options.dev ? b`let { $$slots: #slots = {}, $$scope } = $$props;` : null}

${slots}

${instance_javascript}

${unknown_props_check}

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

${renderer.binding_groups.length > 0 && b`const $$binding_groups = [${renderer.binding_groups.map(_ => x`[]`)}];`}

@@ -441,8 +458,10 @@ export default function dom(

${/* before reactive declarations */ props_inject}

${reactive_declarations.length > 0 && b`
${(reactive_declarations.length > 0 || uses_slots) && b`
$$self.$$.update = () => {
${slots_update}

${reactive_declarations}
};
`}
1 change: 1 addition & 0 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
@@ -320,6 +320,7 @@ export default class ElementWrapper extends Wrapper {
block.chunks.destroy.push(b`@detach(${node});`);
}
} else {
block.root_nodes.push(node);
block.chunks.mount.push(b`@insert(#target, ${node}, anchor);`);

// TODO we eventually need to consider what happens to elements
Original file line number Diff line number Diff line change
@@ -133,6 +133,8 @@ export default class InlineComponentWrapper extends Wrapper {

const name = this.var;

if (parent_node === null) block.root_nodes.push(name);

const component_opts = x`{}` as ObjectExpression;

const statements: Array<Node | Node[]> = [];
2 changes: 1 addition & 1 deletion src/compiler/compile/render_dom/wrappers/Slot.ts
Original file line number Diff line number Diff line change
@@ -128,7 +128,7 @@ export default class SlotWrapper extends Wrapper {
const slot_or_fallback = has_fallback ? block.get_unique_name(`${sanitize(slot_name)}_slot_or_fallback`) : slot;

block.chunks.init.push(b`
const ${slot_definition} = ${renderer.reference('$$slots')}.${slot_name};
const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name};
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}
`);
2 changes: 2 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Text.ts
Original file line number Diff line number Diff line change
@@ -44,6 +44,8 @@ export default class TextWrapper extends Wrapper {
if (this.skip) return;
const use_space = this.use_space();

if (parent_node === null) block.root_nodes.push(this.var);

block.add_element(
this.var,
use_space ? x`@space()` : x`@text("${this.data}")`,
2 changes: 1 addition & 1 deletion src/compiler/compile/utils/reserved_keywords.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const reserved_keywords = new Set(["$$props", "$$restProps"]);
export const reserved_keywords = new Set(["$$props", "$$restProps", "$$slots"]);

export function is_reserved_keyword(name) {
return reserved_keywords.has(name);
2 changes: 1 addition & 1 deletion src/runtime/internal/Component.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { transition_in } from './transitions';
interface Fragment {
key: string|null;
first: null;
/* create */ c: () => void;
/* create */ c: () => void|any[];
/* claim */ l: (nodes: any) => void;
/* hydrate */ h: () => void;
/* mount */ m: (target: HTMLElement, anchor: any) => void;
1 change: 1 addition & 0 deletions src/runtime/internal/index.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ export * from './keyed_each';
export * from './lifecycle';
export * from './loop';
export * from './scheduler';
export * from './slots';
export * from './spread';
export * from './ssr';
export * from './transitions';
83 changes: 83 additions & 0 deletions src/runtime/internal/slots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { onDestroy } from './lifecycle';
import { assign, noop } from './utils';

export function create_slot(definition, ctx, $$scope, fn) {
if (definition) {
const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);
return definition[0](slot_ctx);
}
}

export function get_slot_context(definition, ctx, $$scope, fn) {
return definition[1] && fn
? assign($$scope.ctx.slice(), definition[1](fn(ctx)))
: $$scope.ctx;
}

export function get_slot_changes(definition, $$scope, dirty, fn) {
if (definition[2] && fn) {
const lets = definition[2](fn(dirty));

if ($$scope.dirty === undefined) {
return lets;
}

if (typeof lets === 'object') {
const merged = [];
const len = Math.max($$scope.dirty.length, lets.length);
for (let i = 0; i < len; i += 1) {
merged[i] = $$scope.dirty[i] | lets[i];
}

return merged;
}

return $$scope.dirty | lets;
}

return $$scope.dirty;
}

export function create_slots_accessor(slots, scope) {
const update_list = [];
function update(scope) {
update_list.forEach(fn => fn(scope));
}

const $$slots = {};
for (const key in slots) {
$$slots[key] = function (ctx, callback = noop) {
const definition = slots[key];
const slot = create_slot(definition, null, scope, () => ctx);
const content = slot.c();

function local_update (scope, ctx_fn, dirty_fn) {
slot.p(
get_slot_context(definition, null, scope, ctx_fn),
get_slot_changes(definition, scope, 0, dirty_fn)
);
callback(content);
}

if (slot.d) onDestroy(slot.d);
if (slot.p) update_list.push(local_update);

return {
content,
mount: slot.m,
update: props => local_update(
scope,
() => assign(ctx, props),
() => Object.keys(props).reduce((o, k) => (o[k] = true, o), {})
),
destroy: () => {
slot.d();
const i = update_list.indexOf(local_update);
if (i !== -1) update_list.splice(i, 1);
}
};
};
}

return { $$slots, update };
}
39 changes: 1 addition & 38 deletions src/runtime/internal/utils.ts
Original file line number Diff line number Diff line change
@@ -66,43 +66,6 @@ export function component_subscribe(component, store, callback) {
component.$$.on_destroy.push(subscribe(store, callback));
}

export function create_slot(definition, ctx, $$scope, fn) {
if (definition) {
const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);
return definition[0](slot_ctx);
}
}

export function get_slot_context(definition, ctx, $$scope, fn) {
return definition[1] && fn
? assign($$scope.ctx.slice(), definition[1](fn(ctx)))
: $$scope.ctx;
}

export function get_slot_changes(definition, $$scope, dirty, fn) {
if (definition[2] && fn) {
const lets = definition[2](fn(dirty));

if ($$scope.dirty === undefined) {
return lets;
}

if (typeof lets === 'object') {
const merged = [];
const len = Math.max($$scope.dirty.length, lets.length);
for (let i = 0; i < len; i += 1) {
merged[i] = $$scope.dirty[i] | lets[i];
}

return merged;
}

return $$scope.dirty | lets;
}

return $$scope.dirty;
}

export function exclude_internal_props(props) {
const result = {};
for (const k in props) if (k[0] !== '$') result[k] = props[k];
@@ -138,4 +101,4 @@ export const has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj,

export function action_destroyer(action_result) {
return action_result && is_function(action_result.destroy) ? action_result.destroy : noop;
}
}
4 changes: 2 additions & 2 deletions test/js/samples/capture-inject-state/expected.js
Original file line number Diff line number Diff line change
@@ -103,6 +103,7 @@ function instance($$self, $$props, $$invalidate) {
$$subscribe_prop = () => ($$unsubscribe_prop(), $$unsubscribe_prop = subscribe(prop, $$value => $$invalidate(2, $prop = $$value)), prop);

$$self.$$.on_destroy.push(() => $$unsubscribe_prop());
let { $$slots: slots = {}, $$scope } = $$props;
let { prop } = $$props;
validate_store(prop, "prop");
$$subscribe_prop();
@@ -115,8 +116,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

$$self.$set = $$props => {
if ("prop" in $$props) $$subscribe_prop($$invalidate(0, prop = $$props.prop));
4 changes: 2 additions & 2 deletions test/js/samples/debug-empty/expected.js
Original file line number Diff line number Diff line change
@@ -69,15 +69,15 @@ function create_fragment(ctx) {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let { name } = $$props;
const writable_props = ["name"];

Object.keys($$props).forEach(key => {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

$$self.$set = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
4 changes: 2 additions & 2 deletions test/js/samples/debug-foo-bar-baz-things/expected.js
Original file line number Diff line number Diff line change
@@ -170,6 +170,7 @@ function create_fragment(ctx) {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let { things } = $$props;
let { foo } = $$props;
let { bar } = $$props;
@@ -180,8 +181,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

$$self.$set = $$props => {
if ("things" in $$props) $$invalidate(0, things = $$props.things);
4 changes: 2 additions & 2 deletions test/js/samples/debug-foo/expected.js
Original file line number Diff line number Diff line change
@@ -164,6 +164,7 @@ function create_fragment(ctx) {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let { things } = $$props;
let { foo } = $$props;
const writable_props = ["things", "foo"];
@@ -172,8 +173,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

$$self.$set = $$props => {
if ("things" in $$props) $$invalidate(0, things = $$props.things);
4 changes: 2 additions & 2 deletions test/js/samples/debug-hoisted/expected.js
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ function create_fragment(ctx) {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let obj = { x: 5 };
let kobzol = 5;
const writable_props = [];
@@ -57,8 +58,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);
$$self.$capture_state = () => ({ obj, kobzol });

$$self.$inject_state = $$props => {
4 changes: 2 additions & 2 deletions test/js/samples/debug-no-dependencies/expected.js
Original file line number Diff line number Diff line change
@@ -136,14 +136,14 @@ function create_fragment(ctx) {
}

function instance($$self, $$props) {
let { $$slots: slots = {}, $$scope } = $$props;
const writable_props = [];

Object.keys($$props).forEach(key => {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);
return [];
}

4 changes: 2 additions & 2 deletions test/js/samples/dev-warning-missing-data-computed/expected.js
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ function create_fragment(ctx) {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let { foo } = $$props;
let bar;
const writable_props = ["foo"];
@@ -73,8 +74,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

$$self.$set = $$props => {
if ("foo" in $$props) $$invalidate(0, foo = $$props.foo);
4 changes: 2 additions & 2 deletions test/js/samples/loop-protect/expected.js
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ function foo() {
}

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
let node;

{
@@ -111,8 +112,7 @@ function instance($$self, $$props, $$invalidate) {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console_1.warn(`<Component> was created with unknown prop '${key}'`);
});

let { $$slots = {}, $$scope } = $$props;
validate_slots("Component", $$slots, []);
validate_slots("Component", slots, []);

function div_binding($$value) {
binding_callbacks[$$value ? "unshift" : "push"](() => {
8 changes: 8 additions & 0 deletions test/runtime/samples/$$slots-let/Child.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
const slot = $$slots.default({ value: 'foo' })
let target
$: target && slot.mount(target)
</script>

<button on:click={e => slot.update({ value: 'bar' })}>Click me</button>
<div bind:this={target} />
18 changes: 18 additions & 0 deletions test/runtime/samples/$$slots-let/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default {
async test({ assert, target, window, }) {
assert.htmlEqual(target.innerHTML, `
<button>Click me</button>
<div>foo</div>
`);

const btn = target.querySelector('button');
const clickEvent = new window.MouseEvent('click');

await btn.dispatchEvent(clickEvent);

assert.htmlEqual(target.innerHTML, `
<button>Click me</button>
<div>bar</div>
`);
}
};
7 changes: 7 additions & 0 deletions test/runtime/samples/$$slots-let/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import Child from './Child.svelte'
</script>

<Child let:value>
{value}
</Child>
15 changes: 15 additions & 0 deletions test/runtime/samples/$$slots/Child.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
let target;
const slot = $$slots.default();
$: {
if (target) {
slot.content[0].setAttribute('attr', 'value');
slot.content[2].$set({ prop: 'bar' })
slot.mount(target);
}
}
</script>

<div bind:this={target} />
5 changes: 5 additions & 0 deletions test/runtime/samples/$$slots/Component.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
export let prop
</script>

<p>{prop}</p>
18 changes: 18 additions & 0 deletions test/runtime/samples/$$slots/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default {
async test({ assert, target, window, }) {
assert.htmlEqual(target.innerHTML, `
<button>Click me</button>
<div><p attr="value">Value: a</p><p>bar</p></div>
`);

const btn = target.querySelector('button');
const clickEvent = new window.MouseEvent('click');

await btn.dispatchEvent(clickEvent);

assert.htmlEqual(target.innerHTML, `
<button>Click me</button>
<div><p attr="value">Value: b</p><p>bar</p></div>
`);
}
};
12 changes: 12 additions & 0 deletions test/runtime/samples/$$slots/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import Child from './Child.svelte'
import Component from './Component.svelte'
let value = 'a';
</script>

<button on:click={e => value = 'b'}>Click me</button>
<Child>
<p>Value: {value}</p>
<Component prop="foo" />
</Child>