From 4e525d9e110d1273b78ea93e5f5026e1b37479fe Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Thu, 26 Mar 2020 12:18:36 -0700 Subject: [PATCH] $$slots accessor --- src/compiler/compile/render_dom/Block.ts | 7 ++ src/compiler/compile/render_dom/Renderer.ts | 2 +- src/compiler/compile/render_dom/index.ts | 25 +++++-- .../render_dom/wrappers/Element/index.ts | 1 + .../wrappers/InlineComponent/index.ts | 2 + .../compile/render_dom/wrappers/Slot.ts | 2 +- .../compile/render_dom/wrappers/Text.ts | 2 + .../compile/utils/reserved_keywords.ts | 2 +- src/runtime/internal/Component.ts | 2 +- src/runtime/internal/index.ts | 1 + src/runtime/internal/slots.ts | 69 +++++++++++++++++++ src/runtime/internal/utils.ts | 39 +---------- test/runtime/samples/$$slots/Child.svelte | 15 ++++ test/runtime/samples/$$slots/Component.svelte | 5 ++ test/runtime/samples/$$slots/_config.js | 18 +++++ test/runtime/samples/$$slots/main.svelte | 12 ++++ 16 files changed, 157 insertions(+), 47 deletions(-) create mode 100644 src/runtime/internal/slots.ts create mode 100644 test/runtime/samples/$$slots/Child.svelte create mode 100644 test/runtime/samples/$$slots/Component.svelte create mode 100644 test/runtime/samples/$$slots/_config.js create mode 100644 test/runtime/samples/$$slots/main.svelte diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index f5c4281710..0c3af6da27 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -75,6 +75,8 @@ export default class Block { variables: Map = 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} }`; } diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index ca93c51060..4e48f4fd67 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -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) { diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 4009c6bddf..d69c94fd0d 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -70,6 +70,14 @@ export default function dom( ); } + const uses_slots = component.var_lookup.has('$$slots'); + let slots = null + + if (uses_slots) { + slots = b`let { $$slots, update: #update_$$slots } = @create_slots_accessor(#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 +91,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 +100,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 +428,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 +452,12 @@ export default function dom( ${/* before reactive declarations */ props_inject} - ${reactive_declarations.length > 0 && b` + ${(reactive_declarations.length > 0 || uses_slots) && b` $$self.$$.update = () => { + if (${renderer.dirty(['$$scope'], true)}) { + #update_$$slots($$scope, $$self.$$.dirty) + } + ${reactive_declarations} }; `} diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 9291f329b6..979b9057b7 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -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 diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 4b1e787cbe..45d605f4c4 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -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 = []; diff --git a/src/compiler/compile/render_dom/wrappers/Slot.ts b/src/compiler/compile/render_dom/wrappers/Slot.ts index 1111a7cffe..5e1095d341 100644 --- a/src/compiler/compile/render_dom/wrappers/Slot.ts +++ b/src/compiler/compile/render_dom/wrappers/Slot.ts @@ -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} `); diff --git a/src/compiler/compile/render_dom/wrappers/Text.ts b/src/compiler/compile/render_dom/wrappers/Text.ts index 7ef8aebd70..3a8a230eb0 100644 --- a/src/compiler/compile/render_dom/wrappers/Text.ts +++ b/src/compiler/compile/render_dom/wrappers/Text.ts @@ -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}")`, diff --git a/src/compiler/compile/utils/reserved_keywords.ts b/src/compiler/compile/utils/reserved_keywords.ts index 75825c1719..d6eb8b9673 100644 --- a/src/compiler/compile/utils/reserved_keywords.ts +++ b/src/compiler/compile/utils/reserved_keywords.ts @@ -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); diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 7d2a92fa1b..b6134d1a62 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -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; diff --git a/src/runtime/internal/index.ts b/src/runtime/internal/index.ts index e1dd2a1fcf..b62c560c27 100644 --- a/src/runtime/internal/index.ts +++ b/src/runtime/internal/index.ts @@ -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'; diff --git a/src/runtime/internal/slots.ts b/src/runtime/internal/slots.ts new file mode 100644 index 0000000000..41ab083774 --- /dev/null +++ b/src/runtime/internal/slots.ts @@ -0,0 +1,69 @@ +import { onDestroy } from './lifecycle'; +import { assign } 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 slot_list = []; + function update(scope, dirty) { + slot_list.forEach(({ slot, definition }) => + slot.p( + get_slot_context(definition, [], scope, null), + get_slot_changes(definition, scope, dirty, null) + ) + ); + } + + const $$slots = {}; + for (const key in slots) { + $$slots[key] = function () { + let definition = slots[key]; + let slot = create_slot(definition, [], scope, null); + + if (slot.d) onDestroy(slot.d); + if (slot.p) slot_list.push({ definition, slot }); + + return { + content: slot.c(), + mount: slot.m + }; + }; + } + + return { $$slots, update }; +} diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 487116b655..4be8efdc75 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -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; -} \ No newline at end of file +} diff --git a/test/runtime/samples/$$slots/Child.svelte b/test/runtime/samples/$$slots/Child.svelte new file mode 100644 index 0000000000..ddf49aa6e8 --- /dev/null +++ b/test/runtime/samples/$$slots/Child.svelte @@ -0,0 +1,15 @@ + + +
diff --git a/test/runtime/samples/$$slots/Component.svelte b/test/runtime/samples/$$slots/Component.svelte new file mode 100644 index 0000000000..341bc29318 --- /dev/null +++ b/test/runtime/samples/$$slots/Component.svelte @@ -0,0 +1,5 @@ + + +

{prop}

diff --git a/test/runtime/samples/$$slots/_config.js b/test/runtime/samples/$$slots/_config.js new file mode 100644 index 0000000000..999fb03589 --- /dev/null +++ b/test/runtime/samples/$$slots/_config.js @@ -0,0 +1,18 @@ +export default { + async test({ assert, target, window, }) { + assert.htmlEqual(target.innerHTML, ` + +

Value: a

bar

+ `) + + const btn = target.querySelector('button'); + const clickEvent = new window.MouseEvent('click'); + + await btn.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ` + +

Value: b

bar

+ `); + } +}; diff --git a/test/runtime/samples/$$slots/main.svelte b/test/runtime/samples/$$slots/main.svelte new file mode 100644 index 0000000000..26e1d53202 --- /dev/null +++ b/test/runtime/samples/$$slots/main.svelte @@ -0,0 +1,12 @@ + + + + +

Value: {value}

+ +