diff --git a/.eslintignore b/.eslintignore index 97a855e951..04940e2f7d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,13 @@ **/_actual.js **/expected.js test/*/samples/*/output.js + +# output files +animate/*.js +esing/*.js +internal/*.js +motion/*.js +store/*.js +transition/*.js +index.js +compiler.js diff --git a/site/content/tutorial/04-logic/05-keyed-each-blocks/text.md b/site/content/tutorial/04-logic/05-keyed-each-blocks/text.md index e8bac7d8df..3c204d1413 100644 --- a/site/content/tutorial/04-logic/05-keyed-each-blocks/text.md +++ b/site/content/tutorial/04-logic/05-keyed-each-blocks/text.md @@ -4,7 +4,7 @@ title: Keyed each blocks By default, when you modify the value of an `each` block, it will add and remove items at the *end* of the block, and update any values that have changed. That might not be what you want. -It's easier to show why than to explain. Click the 'Remove first item' button a few times, and notice that it's removing `` components from the end and updating the `value` for those that remain. Instead, we'd like to remove the first `` component and leave the rest unaffected. +It's easier to show why than to explain. Click the 'Remove first thing' button a few times, and notice that it's removing `` components from the end and updating the `value` for those that remain. Instead, we'd like to remove the first `` component and leave the rest unaffected. To do that, we specify a unique identifier for the `each` block: @@ -16,4 +16,4 @@ To do that, we specify a unique identifier for the `each` block: The `(thing.id)` tells Svelte how to figure out what changed. -> You can use any object as the key, as Svelte uses a `Map` internally — in other words you could do `(thing)` instead of `(thing.id)`. Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server. \ No newline at end of file +> You can use any object as the key, as Svelte uses a `Map` internally — in other words you could do `(thing)` instead of `(thing.id)`. Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server. diff --git a/site/content/tutorial/06-bindings/12-bind-this/text.md b/site/content/tutorial/06-bindings/12-bind-this/text.md index e53b624057..4784b6b13f 100644 --- a/site/content/tutorial/06-bindings/12-bind-this/text.md +++ b/site/content/tutorial/06-bindings/12-bind-this/text.md @@ -7,8 +7,8 @@ The readonly `this` binding applies to every element (and component) and allows ```html ``` diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 06d8d5593f..ca93e61d62 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -50,7 +50,6 @@ export default class EachBlockWrapper extends Wrapper { fragment: FragmentWrapper; else?: ElseBlockWrapper; vars: { - anchor: string; create_each_block: string; each_block_value: string; get_each_context: string; @@ -119,10 +118,7 @@ export default class EachBlockWrapper extends Wrapper { // optimisation for array literal fixed_length, data_length: fixed_length === null ? `${each_block_value}.[✂${c}-${c+4}✂]` : fixed_length, - view_length: fixed_length === null ? `${iterations}.[✂${c}-${c+4}✂]` : fixed_length, - - // filled out later - anchor: null + view_length: fixed_length === null ? `${iterations}.[✂${c}-${c+4}✂]` : fixed_length }; node.contexts.forEach(prop => { @@ -175,10 +171,6 @@ export default class EachBlockWrapper extends Wrapper { ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node(); - this.vars.anchor = needs_anchor - ? block.get_unique_name(`${this.var}_anchor`) - : (this.next && this.next.var) || 'null'; - this.context_props = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = ${attach_head('list[i]', prop.tail)};`); if (this.node.has_binding) this.context_props.push(`child_ctx.${this.vars.each_block_value} = list;`); @@ -196,10 +188,28 @@ export default class EachBlockWrapper extends Wrapper { } `); + const initial_anchor_node = parent_node ? 'null' : 'anchor'; + const initial_mount_node = parent_node || '#target'; + const update_anchor_node = needs_anchor + ? block.get_unique_name(`${this.var}_anchor`) + : (this.next && this.next.var) || 'null'; + const update_mount_node = this.get_update_mount_node(update_anchor_node); + + const args = { + block, + parent_node, + parent_nodes, + snippet, + initial_anchor_node, + initial_mount_node, + update_anchor_node, + update_mount_node + }; + if (this.node.key) { - this.render_keyed(block, parent_node, parent_nodes, snippet); + this.render_keyed(args); } else { - this.render_unkeyed(block, parent_node, parent_nodes, snippet); + this.render_unkeyed(args); } if (this.block.has_intro_method || this.block.has_outro_method) { @@ -210,7 +220,7 @@ export default class EachBlockWrapper extends Wrapper { if (needs_anchor) { block.add_element( - this.vars.anchor, + update_anchor_node, `@empty()`, parent_nodes && `@empty()`, parent_node @@ -232,12 +242,10 @@ export default class EachBlockWrapper extends Wrapper { block.builders.mount.add_block(deindent` if (${each_block_else}) { - ${each_block_else}.m(${parent_node || '#target'}, null); + ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node}); } `); - const initial_mount_node = parent_node || `${this.vars.anchor}.parentNode`; - if (this.else.block.has_update_method) { block.builders.update.add_block(deindent` if (!${this.vars.data_length} && ${each_block_else}) { @@ -245,7 +253,7 @@ export default class EachBlockWrapper extends Wrapper { } else if (!${this.vars.data_length}) { ${each_block_else} = ${this.else.block.name}(ctx); ${each_block_else}.c(); - ${each_block_else}.m(${initial_mount_node}, ${this.vars.anchor}); + ${each_block_else}.m(${update_mount_node}, ${update_anchor_node}); } else if (${each_block_else}) { ${each_block_else}.d(1); ${each_block_else} = null; @@ -261,7 +269,7 @@ export default class EachBlockWrapper extends Wrapper { } else if (!${each_block_else}) { ${each_block_else} = ${this.else.block.name}(ctx); ${each_block_else}.c(); - ${each_block_else}.m(${initial_mount_node}, ${this.vars.anchor}); + ${each_block_else}.m(${update_mount_node}, ${update_anchor_node}); } `); } @@ -278,16 +286,28 @@ export default class EachBlockWrapper extends Wrapper { } } - render_keyed( + render_keyed({ + block, + parent_node, + parent_nodes, + snippet, + initial_anchor_node, + initial_mount_node, + update_anchor_node, + update_mount_node + }: { block: Block, parent_node: string, parent_nodes: string, - snippet: string - ) { + snippet: string, + initial_anchor_node: string, + initial_mount_node: string, + update_anchor_node: string, + update_mount_node: string + }) { const { create_each_block, length, - anchor, iterations, view_length } = this.vars; @@ -322,10 +342,6 @@ export default class EachBlockWrapper extends Wrapper { } `); - const initial_mount_node = parent_node || '#target'; - const update_mount_node = this.get_update_mount_node(anchor); - const anchor_node = parent_node ? 'null' : 'anchor'; - block.builders.create.add_block(deindent` for (#i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].c(); `); @@ -337,7 +353,7 @@ export default class EachBlockWrapper extends Wrapper { } block.builders.mount.add_block(deindent` - for (#i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].m(${initial_mount_node}, ${anchor_node}); + for (#i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node}); `); const dynamic = this.block.has_update_method; @@ -355,7 +371,7 @@ export default class EachBlockWrapper extends Wrapper { ${this.block.has_outros && `@group_outros();`} ${this.node.has_animation && `for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`} - ${iterations} = @update_keyed_each(${iterations}, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${anchor}, ${this.vars.get_each_context}); + ${iterations} = @update_keyed_each(${iterations}, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context}); ${this.node.has_animation && `for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`} ${this.block.has_outros && `@check_outros();`} `); @@ -371,20 +387,30 @@ export default class EachBlockWrapper extends Wrapper { `); } - render_unkeyed( + render_unkeyed({ + block, + parent_nodes, + snippet, + initial_anchor_node, + initial_mount_node, + update_anchor_node, + update_mount_node + }: { block: Block, - parent_node: string, parent_nodes: string, - snippet: string - ) { + snippet: string, + initial_anchor_node: string, + initial_mount_node: string, + update_anchor_node: string, + update_mount_node: string + }) { const { create_each_block, length, iterations, fixed_length, data_length, - view_length, - anchor + view_length } = this.vars; block.builders.init.add_block(deindent` @@ -395,10 +421,6 @@ export default class EachBlockWrapper extends Wrapper { } `); - const initial_mount_node = parent_node || '#target'; - const update_mount_node = this.get_update_mount_node(anchor); - const anchor_node = parent_node ? 'null' : 'anchor'; - block.builders.create.add_block(deindent` for (var #i = 0; #i < ${view_length}; #i += 1) { ${iterations}[#i].c(); @@ -415,7 +437,7 @@ export default class EachBlockWrapper extends Wrapper { block.builders.mount.add_block(deindent` for (var #i = 0; #i < ${view_length}; #i += 1) { - ${iterations}[#i].m(${initial_mount_node}, ${anchor_node}); + ${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node}); } `); @@ -441,7 +463,7 @@ export default class EachBlockWrapper extends Wrapper { ${iterations}[#i] = ${create_each_block}(child_ctx); ${iterations}[#i].c(); ${has_transitions && `@transition_in(${this.vars.iterations}[#i], 1);`} - ${iterations}[#i].m(${update_mount_node}, ${anchor}); + ${iterations}[#i].m(${update_mount_node}, ${update_anchor_node}); } ` : has_transitions @@ -452,14 +474,14 @@ export default class EachBlockWrapper extends Wrapper { ${iterations}[#i] = ${create_each_block}(child_ctx); ${iterations}[#i].c(); @transition_in(${this.vars.iterations}[#i], 1); - ${iterations}[#i].m(${update_mount_node}, ${anchor}); + ${iterations}[#i].m(${update_mount_node}, ${update_anchor_node}); } ` : deindent` if (!${iterations}[#i]) { ${iterations}[#i] = ${create_each_block}(child_ctx); ${iterations}[#i].c(); - ${iterations}[#i].m(${update_mount_node}, ${anchor}); + ${iterations}[#i].m(${update_mount_node}, ${update_anchor_node}); } `; diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 9179225e29..9b14d1cd6c 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -19,6 +19,7 @@ import add_event_handlers from '../shared/add_event_handlers'; import add_actions from '../shared/add_actions'; import create_debugging_comment from '../shared/create_debugging_comment'; import { get_context_merger } from '../shared/get_context_merger'; +import bind_this from '../shared/bind_this'; const events = [ { @@ -540,38 +541,9 @@ export default class ElementWrapper extends Wrapper { const this_binding = this.bindings.find(b => b.node.name === 'this'); if (this_binding) { - const name = renderer.component.get_unique_name(`${this.var}_binding`); + const binding_callback = bind_this(renderer.component, block, this_binding.node, this.var); - renderer.component.add_var({ - name, - internal: true, - referenced: true - }); - - const { handler, object } = this_binding; - - const args = []; - for (const arg of handler.contextual_dependencies) { - args.push(arg); - block.add_variable(arg, `ctx.${arg}`); - } - - renderer.component.partly_hoisted.push(deindent` - function ${name}(${['$$node', 'check'].concat(args).join(', ')}) { - ${handler.snippet ? `if ($$node || (!$$node && ${handler.snippet} === check)) ` : ''}${handler.mutation} - ${renderer.component.invalidate(object)}; - } - `); - - block.builders.mount.add_line(`@add_binding_callback(() => ctx.${name}(${[this.var, 'null'].concat(args).join(', ')}));`); - block.builders.destroy.add_line(`ctx.${name}(${['null', this.var].concat(args).join(', ')});`); - block.builders.update.add_line(deindent` - if (changed.items) { - ctx.${name}(${['null', this.var].concat(args).join(', ')}); - ${args.map(a => `${a} = ctx.${a}`).join(', ')}; - ctx.${name}(${[this.var, 'null'].concat(args).join(', ')}); - }` - ); + block.builders.mount.add_line(binding_callback); } } diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 347843ad04..9cff40ecbb 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -9,12 +9,12 @@ import add_to_set from '../../../utils/add_to_set'; import deindent from '../../../utils/deindent'; import Attribute from '../../../nodes/Attribute'; import get_object from '../../../utils/get_object'; -import flatten_reference from '../../../utils/flatten_reference'; import create_debugging_comment from '../shared/create_debugging_comment'; import { get_context_merger } from '../shared/get_context_merger'; import EachBlock from '../../../nodes/EachBlock'; import TemplateScope from '../../../nodes/shared/TemplateScope'; import is_dynamic from '../shared/is_dynamic'; +import bind_this from '../shared/bind_this'; export default class InlineComponentWrapper extends Wrapper { var: string; @@ -249,41 +249,7 @@ export default class InlineComponentWrapper extends Wrapper { component.has_reactive_assignments = true; if (binding.name === 'this') { - const fn = component.get_unique_name(`${this.var}_binding`); - - component.add_var({ - name: fn, - internal: true, - referenced: true - }); - - let lhs; - let object; - - if (binding.is_contextual && binding.expression.node.type === 'Identifier') { - // bind:x={y} — we can't just do `y = x`, we need to - // to `array[index] = x; - const { name } = binding.expression.node; - const { snippet } = block.bindings.get(name); - lhs = snippet; - - // TODO we need to invalidate... something - } else { - object = flatten_reference(binding.expression.node).name; - lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim(); - } - - const contextual_dependencies = [...binding.expression.contextual_dependencies]; - - component.partly_hoisted.push(deindent` - function ${fn}(${['$$component', ...contextual_dependencies].join(', ')}) { - ${lhs} = $$component; - ${object && component.invalidate(object)} - } - `); - - block.builders.destroy.add_line(`ctx.${fn}(null);`); - return `@add_binding_callback(() => ctx.${fn}(${[this.var, ...contextual_dependencies.map(name => `ctx.${name}`)].join(', ')}));`; + return bind_this(component, block, binding, this.var); } const name = component.get_unique_name(`${this.var}_${binding.name}_binding`); diff --git a/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts new file mode 100644 index 0000000000..3d08cfaff5 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/bind_this.ts @@ -0,0 +1,80 @@ +import flatten_reference from '../../../utils/flatten_reference'; +import deindent from '../../../utils/deindent'; +import Component from '../../../Component'; +import Block from '../../Block'; +import Binding from '../../../nodes/Binding'; + +export default function bind_this(component: Component, block: Block, binding: Binding, variable: string) { + const fn = component.get_unique_name(`${variable}_binding`); + + component.add_var({ + name: fn, + internal: true, + referenced: true + }); + + let lhs; + let object; + + if (binding.is_contextual && binding.expression.node.type === 'Identifier') { + // bind:x={y} — we can't just do `y = x`, we need to + // to `array[index] = x; + const { name } = binding.expression.node; + const { snippet } = block.bindings.get(name); + lhs = snippet; + + // TODO we need to invalidate... something + } else { + object = flatten_reference(binding.expression.node).name; + lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim(); + } + + const contextual_dependencies = Array.from(binding.expression.contextual_dependencies); + + if (contextual_dependencies.length) { + component.partly_hoisted.push(deindent` + function ${fn}(${['$$value', ...contextual_dependencies].join(', ')}) { + if (${lhs} === $$value) return; + ${lhs} = $$value; + ${object && component.invalidate(object)} + } + `); + + const args = []; + for (const arg of contextual_dependencies) { + args.push(arg); + block.add_variable(arg, `ctx.${arg}`); + } + + const assign = block.get_unique_name(`assign_${variable}`); + const unassign = block.get_unique_name(`unassign_${variable}`); + + block.builders.init.add_block(deindent` + const ${assign} = () => ctx.${fn}(${[variable].concat(args).join(', ')}); + const ${unassign} = () => ctx.${fn}(${['null'].concat(args).join(', ')}); + `); + + const condition = Array.from(contextual_dependencies).map(name => `${name} !== ctx.${name}`).join(' || '); + + block.builders.update.add_line(deindent` + if (${condition}) { + ${unassign}(); + ${args.map(a => `${a} = ctx.${a}`).join(', ')}; + @add_binding_callback(${assign}); + }` + ); + + block.builders.destroy.add_line(`${unassign}();`); + return `@add_binding_callback(${assign});`; + } + + component.partly_hoisted.push(deindent` + function ${fn}($$value) { + ${lhs} = $$value; + ${object && component.invalidate(object)} + } + `); + + block.builders.destroy.add_line(`ctx.${fn}(null);`); + return `@add_binding_callback(() => ctx.${fn}(${variable}));`; +} \ No newline at end of file diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index a26a4f8c33..9e1b4280bf 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -46,7 +46,7 @@ export function flush() { update(component.$$); } - while (binding_callbacks.length) binding_callbacks.shift()(); + while (binding_callbacks.length) binding_callbacks.pop()(); // then, once components are updated, call // afterUpdate functions. This may cause diff --git a/test/runtime/samples/binding-this-each-block-property-component/Foo.svelte b/test/runtime/samples/binding-this-each-block-property-component/Foo.svelte new file mode 100644 index 0000000000..dc8b5f206c --- /dev/null +++ b/test/runtime/samples/binding-this-each-block-property-component/Foo.svelte @@ -0,0 +1,7 @@ + + +

\ No newline at end of file diff --git a/test/runtime/samples/binding-this-each-block-property-component/_config.js b/test/runtime/samples/binding-this-each-block-property-component/_config.js new file mode 100644 index 0000000000..947ec17929 --- /dev/null +++ b/test/runtime/samples/binding-this-each-block-property-component/_config.js @@ -0,0 +1,12 @@ +export default { + html: ``, + + async test({ assert, component, target }) { + component.visible = true; + assert.htmlEqual(target.innerHTML, ` +

a

+ `); + + assert.ok(component.items[0].ref.isFoo()); + } +}; diff --git a/test/runtime/samples/binding-this-each-block-property-component/main.svelte b/test/runtime/samples/binding-this-each-block-property-component/main.svelte new file mode 100644 index 0000000000..257aa18a91 --- /dev/null +++ b/test/runtime/samples/binding-this-each-block-property-component/main.svelte @@ -0,0 +1,13 @@ + + +{#if visible} + {#each items as item} + {item.value} + {/each} +{/if} diff --git a/test/runtime/samples/binding-this-each-block-property/_config.js b/test/runtime/samples/binding-this-each-block-property/_config.js new file mode 100644 index 0000000000..15ad4be5db --- /dev/null +++ b/test/runtime/samples/binding-this-each-block-property/_config.js @@ -0,0 +1,12 @@ +export default { + html: ``, + + async test({ assert, component, target }) { + component.visible = true; + assert.htmlEqual(target.innerHTML, ` +
a
+ `); + + assert.equal(component.items[0].ref, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/binding-this-each-block-property/main.svelte b/test/runtime/samples/binding-this-each-block-property/main.svelte new file mode 100644 index 0000000000..73ede7ab75 --- /dev/null +++ b/test/runtime/samples/binding-this-each-block-property/main.svelte @@ -0,0 +1,11 @@ + + +{#if visible} + {#each items as item} +
{item.value}
+ {/each} +{/if} diff --git a/test/runtime/samples/each-block-else-in-if/_config.js b/test/runtime/samples/each-block-else-in-if/_config.js new file mode 100644 index 0000000000..c800ee8a30 --- /dev/null +++ b/test/runtime/samples/each-block-else-in-if/_config.js @@ -0,0 +1,17 @@ +export default { + html: ` +

nothing

+

after

+ `, + + test({ assert, component, target }) { + component.visible = false; + assert.htmlEqual(target.innerHTML, ``); + + component.visible = true; + assert.htmlEqual(target.innerHTML, ` +

nothing

+

after

+ `); + } +}; diff --git a/test/runtime/samples/each-block-else-in-if/main.svelte b/test/runtime/samples/each-block-else-in-if/main.svelte new file mode 100644 index 0000000000..d89f645d7f --- /dev/null +++ b/test/runtime/samples/each-block-else-in-if/main.svelte @@ -0,0 +1,14 @@ + + +{#if visible} + {#each empty as item} +

{item}

+ {:else} +

nothing

+ {/each} + +

after

+{/if} \ No newline at end of file